diff --git a/Games/2048/2048.cpp b/Games/2048/2048.cpp deleted file mode 100644 index d408c29c02..0000000000 --- a/Games/2048/2048.cpp +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "2048.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -TwentyFortyEightGame::TwentyFortyEightGame() -{ - srand(time(nullptr)); - reset(); -} - -TwentyFortyEightGame::~TwentyFortyEightGame() -{ -} - -template -void TwentyFortyEightGame::add_tile(Board& board, int max_tile_value) -{ - int row; - int column; - do { - row = rand() % m_rows; - column = rand() % m_columns; - } while (board[row][column] != 0); - - int value = rand() % max_tile_value; - value = round_up_to_power_of_two(value, max_tile_value); - board[row][column] = max(2, value); -} - -void TwentyFortyEightGame::pick_font() -{ - constexpr static auto liza_regular = "Liza Regular"; - String best_font_name = liza_regular; - int best_font_size = -1; - auto& font_database = GUI::FontDatabase::the(); - font_database.for_each_font([&](const StringView& font_name) { - // Only consider variations of Liza Regular. - if (!font_name.starts_with(liza_regular)) - return; - auto metadata = font_database.get_metadata_by_name(font_name); - if (!metadata.has_value()) - return; - auto size = metadata.value().glyph_height; - if (size * 2 <= m_cell_size && size > best_font_size) { - best_font_name = font_name; - best_font_size = size; - } - }); - - auto font = font_database.get_by_name(best_font_name); - set_font(font); -} - -void TwentyFortyEightGame::reset() -{ - auto initial_state = [&]() -> State { - State state; - state.board.resize(m_columns); - auto& board = state.board; - for (auto& row : board) { - row.resize(m_rows); - for (auto& j : row) - j = 0; - } - - add_tile(state.board, m_starting_tile); - add_tile(state.board, m_starting_tile); - - return state; - }; - - m_states.clear(); - m_states.append(initial_state()); - - m_current_turn = 0; - m_states.last().score_text = "Score: 0"; - - update(); -} - -Gfx::IntRect TwentyFortyEightGame::score_rect() const -{ - int score_width = font().width(m_states.last().score_text); - return { 0, 2, score_width, font().glyph_height() }; -} - -static Vector> transpose(const Vector>& board) -{ - Vector> new_board; - auto result_row_count = board[0].size(); - auto result_column_count = board.size(); - - new_board.resize(result_row_count); - - for (size_t i = 0; i < board.size(); ++i) { - auto& row = new_board[i]; - row.clear_with_capacity(); - row.ensure_capacity(result_column_count); - for (auto& entry : board) { - row.append(entry[i]); - } - } - - return new_board; -} - -static Vector> reverse(const Vector>& board) -{ - auto new_board = board; - for (auto& row : new_board) { - for (size_t i = 0; i < row.size() / 2; ++i) - swap(row[i], row[row.size() - i - 1]); - } - - return new_board; -} - -static Vector slide_row(const Vector& row, size_t& successful_merge_score) -{ - if (row.size() < 2) - return row; - - auto x = row[0]; - auto y = row[1]; - - auto result = row; - result.take_first(); - - if (x == 0) { - result = slide_row(result, successful_merge_score); - result.append(0); - return result; - } - - if (y == 0) { - result[0] = x; - result = slide_row(result, successful_merge_score); - result.append(0); - return result; - } - - if (x == y) { - result.take_first(); - result = slide_row(result, successful_merge_score); - result.append(0); - result.prepend(x + x); - successful_merge_score += x * 2; - return result; - } - - result = slide_row(result, successful_merge_score); - result.prepend(x); - return result; -} - -static Vector> slide_left(const Vector>& board, size_t& successful_merge_score) -{ - Vector> new_board; - for (auto& row : board) - new_board.append(slide_row(row, successful_merge_score)); - - return new_board; -} - -static bool is_complete(const TwentyFortyEightGame::State& state) -{ - for (auto& row : state.board) { - if (row.contains_slow(2048)) - return true; - } - - return false; -} - -static bool has_no_neighbors(const Span& row) -{ - if (row.size() < 2) - return true; - - auto x = row[0]; - auto y = row[1]; - - if (x == y) - return false; - - return has_no_neighbors(row.slice(1, row.size() - 1)); -}; - -static bool is_stalled(const TwentyFortyEightGame::State& state) -{ - static auto stalled = [](auto& row) { - return !row.contains_slow(0) && has_no_neighbors(row.span()); - }; - - for (auto& row : state.board) - if (!stalled(row)) - return false; - - for (auto& row : transpose(state.board)) - if (!stalled(row)) - return false; - - return true; -} - -void TwentyFortyEightGame::resize_event(GUI::ResizeEvent&) -{ - int score_height = font().glyph_height() + 2; - - constexpr float padding_ratio = 7; - m_padding = min( - width() / (m_columns * (padding_ratio + 1) + 1), - (height() - score_height) / (m_rows * (padding_ratio + 1) + 1)); - m_cell_size = m_padding * padding_ratio; - - pick_font(); -} - -void TwentyFortyEightGame::keydown_event(GUI::KeyEvent& event) -{ - auto& previous_state = m_states.last(); - State new_state; - size_t successful_merge_score = 0; - switch (event.key()) { - case KeyCode::Key_A: - case KeyCode::Key_Left: - new_state.board = slide_left(previous_state.board, successful_merge_score); - break; - case KeyCode::Key_D: - case KeyCode::Key_Right: - new_state.board = reverse(slide_left(reverse(previous_state.board), successful_merge_score)); - break; - case KeyCode::Key_W: - case KeyCode::Key_Up: - new_state.board = transpose(slide_left(transpose(previous_state.board), successful_merge_score)); - break; - case KeyCode::Key_S: - case KeyCode::Key_Down: - new_state.board = transpose(reverse(slide_left(reverse(transpose(previous_state.board)), successful_merge_score))); - break; - default: - return; - } - - if (new_state.board != previous_state.board) { - ++m_current_turn; - add_tile(new_state.board, m_starting_tile * 2); - auto last_score = m_states.last().score; - if (m_states.size() == 16) - m_states.take_first(); - m_states.append(move(new_state)); - - m_states.last().score = last_score + successful_merge_score; - m_states.last().score_text = String::format("Score: %d", score()); - - update(); - } - - if (is_complete(m_states.last())) { - // You won! - GUI::MessageBox::show(window(), - String::format("Score = %d in %zu turns", score(), m_current_turn), - "You won!", - GUI::MessageBox::Type::Information); - return game_over(); - } - - if (is_stalled(m_states.last())) { - // Game over! - GUI::MessageBox::show(window(), - String::format("Score = %d in %zu turns", score(), m_current_turn), - "You lost!", - GUI::MessageBox::Type::Information); - return game_over(); - } -} - -Gfx::Color TwentyFortyEightGame::background_color_for_cell(u32 value) -{ - switch (value) { - case 0: - return Color::from_rgb(0xcdc1b4); - case 2: - return Color::from_rgb(0xeee4da); - case 4: - return Color::from_rgb(0xede0c8); - case 8: - return Color::from_rgb(0xf2b179); - case 16: - return Color::from_rgb(0xf59563); - case 32: - return Color::from_rgb(0xf67c5f); - case 64: - return Color::from_rgb(0xf65e3b); - case 128: - return Color::from_rgb(0xedcf72); - case 256: - return Color::from_rgb(0xedcc61); - case 512: - return Color::from_rgb(0xedc850); - case 1024: - return Color::from_rgb(0xedc53f); - case 2048: - return Color::from_rgb(0xedc22e); - default: - ASSERT_NOT_REACHED(); - } -} - -Gfx::Color TwentyFortyEightGame::text_color_for_cell(u32 value) -{ - if (value <= 4) - return Color::from_rgb(0x776e65); - return Color::from_rgb(0xf9f6f2); -} - -void TwentyFortyEightGame::paint_event(GUI::PaintEvent&) -{ - Color background_color = Color::from_rgb(0xbbada0); - - GUI::Painter painter(*this); - - painter.draw_text(score_rect(), m_states.last().score_text, font(), Gfx::TextAlignment::TopLeft, palette().color(ColorRole::BaseText)); - - int score_height = font().glyph_height() + 2; - - Gfx::IntRect field_rect { - 0, - 0, - static_cast(m_padding + (m_cell_size + m_padding) * m_columns), - static_cast(m_padding + (m_cell_size + m_padding) * m_rows) - }; - field_rect.center_within({ 0, score_height, width(), height() - score_height }); - painter.fill_rect(field_rect, background_color); - - for (auto column = 0; column < m_columns; ++column) { - for (auto row = 0; row < m_rows; ++row) { - auto rect = Gfx::IntRect { - field_rect.x() + m_padding + (m_cell_size + m_padding) * column, - field_rect.y() + m_padding + (m_cell_size + m_padding) * row, - m_cell_size, - m_cell_size, - }; - auto entry = m_states.last().board[row][column]; - painter.fill_rect(rect, background_color_for_cell(entry)); - if (entry > 0) - painter.draw_text(rect, String::number(entry), font(), Gfx::TextAlignment::Center, text_color_for_cell(entry)); - } - } -} - -void TwentyFortyEightGame::game_over() -{ - reset(); -} - -int TwentyFortyEightGame::score() const -{ - return m_states.last().score; -} - -void TwentyFortyEightGame::undo() -{ - if (m_states.size() > 1) { - m_states.take_last(); - --m_current_turn; - update(); - } -} diff --git a/Games/2048/BoardView.cpp b/Games/2048/BoardView.cpp new file mode 100644 index 0000000000..f3e3524665 --- /dev/null +++ b/Games/2048/BoardView.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "BoardView.h" +#include +#include +#include +#include + +BoardView::BoardView(const Game::Board* board) + : m_board(board) +{ +} + +BoardView::~BoardView() +{ +} + +void BoardView::set_score(size_t score) +{ + if (m_score == score && !m_score_text.is_null()) + return; + + m_score = score; + m_score_text = String::format("Score: %d", score); + update(); +} + +void BoardView::set_board(const Game::Board* board) +{ + if (m_board == board) + return; + + m_board = board; + update(); +} + +void BoardView::pick_font() +{ + constexpr static auto liza_regular = "Liza Regular"; + String best_font_name = liza_regular; + int best_font_size = -1; + auto& font_database = GUI::FontDatabase::the(); + font_database.for_each_font([&](const StringView& font_name) { + // Only consider variations of Liza Regular. + if (!font_name.starts_with(liza_regular)) + return; + auto metadata = font_database.get_metadata_by_name(font_name); + if (!metadata.has_value()) + return; + auto size = metadata.value().glyph_height; + if (size * 2 <= m_cell_size && size > best_font_size) { + best_font_name = font_name; + best_font_size = size; + } + }); + + auto font = font_database.get_by_name(best_font_name); + set_font(font); +} + +Gfx::IntRect BoardView::score_rect() const +{ + int score_width = font().width(m_score_text); + return { 0, 2, score_width, font().glyph_height() }; +} + +size_t BoardView::rows() const +{ + if (!m_board) + return 0; + return m_board->size(); +} + +size_t BoardView::columns() const +{ + if (!m_board) + return 0; + if (m_board->is_empty()) + return 0; + return (*m_board)[0].size(); +} + +void BoardView::resize_event(GUI::ResizeEvent&) +{ + int score_height = font().glyph_height() + 2; + + constexpr float padding_ratio = 7; + m_padding = min( + width() / (columns() * (padding_ratio + 1) + 1), + (height() - score_height) / (rows() * (padding_ratio + 1) + 1)); + m_cell_size = m_padding * padding_ratio; + + pick_font(); +} + +void BoardView::keydown_event(GUI::KeyEvent& event) +{ + if (!on_move) + return; + + switch (event.key()) { + case KeyCode::Key_A: + case KeyCode::Key_Left: + on_move(Game::Direction::Left); + break; + case KeyCode::Key_D: + case KeyCode::Key_Right: + on_move(Game::Direction::Right); + break; + case KeyCode::Key_W: + case KeyCode::Key_Up: + on_move(Game::Direction::Up); + break; + case KeyCode::Key_S: + case KeyCode::Key_Down: + on_move(Game::Direction::Down); + break; + default: + return; + } +} + +Gfx::Color BoardView::background_color_for_cell(u32 value) +{ + switch (value) { + case 0: + return Color::from_rgb(0xcdc1b4); + case 2: + return Color::from_rgb(0xeee4da); + case 4: + return Color::from_rgb(0xede0c8); + case 8: + return Color::from_rgb(0xf2b179); + case 16: + return Color::from_rgb(0xf59563); + case 32: + return Color::from_rgb(0xf67c5f); + case 64: + return Color::from_rgb(0xf65e3b); + case 128: + return Color::from_rgb(0xedcf72); + case 256: + return Color::from_rgb(0xedcc61); + case 512: + return Color::from_rgb(0xedc850); + case 1024: + return Color::from_rgb(0xedc53f); + case 2048: + return Color::from_rgb(0xedc22e); + default: + ASSERT_NOT_REACHED(); + } +} + +Gfx::Color BoardView::text_color_for_cell(u32 value) +{ + if (value <= 4) + return Color::from_rgb(0x776e65); + return Color::from_rgb(0xf9f6f2); +} + +void BoardView::paint_event(GUI::PaintEvent&) +{ + Color background_color = Color::from_rgb(0xbbada0); + + GUI::Painter painter(*this); + + if (!m_board) { + painter.fill_rect(rect(), background_color); + return; + } + auto& board = *m_board; + + painter.draw_text(score_rect(), m_score_text, font(), Gfx::TextAlignment::TopLeft, palette().color(ColorRole::BaseText)); + + int score_height = font().glyph_height() + 2; + + Gfx::IntRect field_rect { + 0, + 0, + static_cast(m_padding + (m_cell_size + m_padding) * columns()), + static_cast(m_padding + (m_cell_size + m_padding) * rows()) + }; + field_rect.center_within({ 0, score_height, width(), height() - score_height }); + painter.fill_rect(field_rect, background_color); + + for (size_t column = 0; column < columns(); ++column) { + for (size_t row = 0; row < rows(); ++row) { + auto rect = Gfx::IntRect { + field_rect.x() + m_padding + (m_cell_size + m_padding) * column, + field_rect.y() + m_padding + (m_cell_size + m_padding) * row, + m_cell_size, + m_cell_size, + }; + auto entry = board[row][column]; + painter.fill_rect(rect, background_color_for_cell(entry)); + if (entry > 0) + painter.draw_text(rect, String::number(entry), font(), Gfx::TextAlignment::Center, text_color_for_cell(entry)); + } + } +} diff --git a/Games/2048/2048.h b/Games/2048/BoardView.h similarity index 74% rename from Games/2048/2048.h rename to Games/2048/BoardView.h index 82deae05b1..ce3af0cdff 100644 --- a/Games/2048/2048.h +++ b/Games/2048/BoardView.h @@ -26,47 +26,39 @@ #pragma once -#include +#include "Game.h" #include -class TwentyFortyEightGame final : public GUI::Widget { - C_OBJECT(TwentyFortyEightGame) +class BoardView final : public GUI::Widget { + C_OBJECT(BoardView) + public: - virtual ~TwentyFortyEightGame() override; + BoardView(const Game::Board*); + virtual ~BoardView() override; - void reset(); - int score() const; - void undo(); + void set_score(size_t score); + void set_board(const Game::Board* board); - struct State { - Vector> board; - size_t score { 0 }; - String score_text; - }; + Function on_move; private: - TwentyFortyEightGame(); virtual void resize_event(GUI::ResizeEvent&) override; virtual void paint_event(GUI::PaintEvent&) override; virtual void keydown_event(GUI::KeyEvent&) override; + size_t rows() const; + size_t columns() const; + void pick_font(); - void game_over(); Gfx::IntRect score_rect() const; - template - void add_tile(Board& board, int max_tile_value); - - int m_rows { 4 }; - int m_columns { 4 }; - u32 m_starting_tile { 2 }; - size_t m_current_turn { 0 }; - Color background_color_for_cell(u32 value); Color text_color_for_cell(u32 value); float m_padding { 0 }; float m_cell_size { 0 }; - Vector m_states; + size_t m_score { 0 }; + String m_score_text; + const Game::Board* m_board { nullptr }; }; diff --git a/Games/2048/CMakeLists.txt b/Games/2048/CMakeLists.txt index 54659c00ba..d239331a44 100644 --- a/Games/2048/CMakeLists.txt +++ b/Games/2048/CMakeLists.txt @@ -1,6 +1,7 @@ set(SOURCES + BoardView.cpp + Game.cpp main.cpp - 2048.cpp ) serenity_bin(2048) diff --git a/Games/2048/Game.cpp b/Games/2048/Game.cpp new file mode 100644 index 0000000000..f5412b0555 --- /dev/null +++ b/Games/2048/Game.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Game.h" + +Game::Game(size_t rows, size_t columns) + : m_rows(rows) + , m_columns(columns) +{ + m_board.resize(rows); + for (auto& row : m_board) { + row.ensure_capacity(columns); + for (size_t i = 0; i < columns; i++) + row.append(0); + } + + add_tile(2); + add_tile(2); +} + +void Game::add_tile(u32 max_tile_value) +{ + int row; + int column; + do { + row = rand() % m_rows; + column = rand() % m_columns; + } while (m_board[row][column] != 0); + + int value = rand() % max_tile_value; + value = round_up_to_power_of_two(value, max_tile_value); + m_board[row][column] = max(2, value); +} + +static Game::Board transpose(const Game::Board& board) +{ + Vector> new_board; + auto result_row_count = board[0].size(); + auto result_column_count = board.size(); + + new_board.resize(result_row_count); + + for (size_t i = 0; i < board.size(); ++i) { + auto& row = new_board[i]; + row.clear_with_capacity(); + row.ensure_capacity(result_column_count); + for (auto& entry : board) { + row.append(entry[i]); + } + } + + return new_board; +} + +static Game::Board reverse(const Game::Board& board) +{ + auto new_board = board; + for (auto& row : new_board) { + for (size_t i = 0; i < row.size() / 2; ++i) + swap(row[i], row[row.size() - i - 1]); + } + + return new_board; +} + +static Vector slide_row(const Vector& row, size_t& successful_merge_score) +{ + if (row.size() < 2) + return row; + + auto x = row[0]; + auto y = row[1]; + + auto result = row; + result.take_first(); + + if (x == 0) { + result = slide_row(result, successful_merge_score); + result.append(0); + return result; + } + + if (y == 0) { + result[0] = x; + result = slide_row(result, successful_merge_score); + result.append(0); + return result; + } + + if (x == y) { + result.take_first(); + result = slide_row(result, successful_merge_score); + result.append(0); + result.prepend(x + x); + successful_merge_score += x * 2; + return result; + } + + result = slide_row(result, successful_merge_score); + result.prepend(x); + return result; +} + +static Game::Board slide_left(const Game::Board& board, size_t& successful_merge_score) +{ + Vector> new_board; + for (auto& row : board) + new_board.append(slide_row(row, successful_merge_score)); + + return new_board; +} + +static bool is_complete(const Game::Board& board) +{ + for (auto& row : board) { + if (row.contains_slow(2048)) + return true; + } + + return false; +} + +static bool has_no_neighbors(const Span& row) +{ + if (row.size() < 2) + return true; + + auto x = row[0]; + auto y = row[1]; + + if (x == y) + return false; + + return has_no_neighbors(row.slice(1, row.size() - 1)); +}; + +static bool is_stalled(const Game::Board& board) +{ + static auto stalled = [](auto& row) { + return !row.contains_slow(0) && has_no_neighbors(row.span()); + }; + + for (auto& row : board) + if (!stalled(row)) + return false; + + for (auto& row : transpose(board)) + if (!stalled(row)) + return false; + + return true; +} + +Game::MoveOutcome Game::attempt_move(Direction direction) +{ + size_t successful_merge_score = 0; + Board new_board; + + switch (direction) { + case Direction::Left: + new_board = slide_left(m_board, successful_merge_score); + break; + case Direction::Right: + new_board = reverse(slide_left(reverse(m_board), successful_merge_score)); + break; + case Direction::Up: + new_board = transpose(slide_left(transpose(m_board), successful_merge_score)); + break; + case Direction::Down: + new_board = transpose(reverse(slide_left(reverse(transpose(m_board)), successful_merge_score))); + break; + } + + bool moved = new_board != m_board; + if (moved) { + m_board = new_board; + m_turns++; + add_tile(4); + m_score += successful_merge_score; + } + + if (is_complete(m_board)) + return MoveOutcome::Won; + if (is_stalled(m_board)) + return MoveOutcome::GameOver; + if (moved) + return MoveOutcome::OK; + return MoveOutcome::InvalidMove; +} diff --git a/Games/2048/Game.h b/Games/2048/Game.h new file mode 100644 index 0000000000..9df56fa0a9 --- /dev/null +++ b/Games/2048/Game.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +class Game final { +public: + Game(size_t rows, size_t columns); + Game(const Game&) = default; + + enum class MoveOutcome { + OK, + InvalidMove, + GameOver, + Won, + }; + + enum class Direction { + Up, + Down, + Left, + Right, + }; + + MoveOutcome attempt_move(Direction); + + size_t score() const { return m_score; } + size_t turns() const { return m_turns; } + + using Board = Vector>; + + const Board& board() const { return m_board; } + +private: + void add_tile(u32 max_tile_value); + + size_t m_rows { 0 }; + size_t m_columns { 0 }; + + Board m_board; + size_t m_score { 0 }; + size_t m_turns { 0 }; +}; diff --git a/Games/2048/main.cpp b/Games/2048/main.cpp index 2d162d0c06..5132788848 100644 --- a/Games/2048/main.cpp +++ b/Games/2048/main.cpp @@ -24,7 +24,8 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "2048.h" +#include "BoardView.h" +#include "Game.h" #include #include #include @@ -32,8 +33,10 @@ #include #include #include +#include #include #include +#include int main(int argc, char** argv) { @@ -42,6 +45,8 @@ int main(int argc, char** argv) return 1; } + srand(time(nullptr)); + auto app = GUI::Application::construct(argc, argv); auto window = GUI::Window::construct(); @@ -65,18 +70,69 @@ int main(int argc, char** argv) window->set_title("2048"); window->resize(324, 336); - auto& game = window->set_main_widget(); - game.set_fill_with_background_color(true); + Game game { 4, 4 }; + + auto& board_view = window->set_main_widget(&game.board()); + board_view.set_fill_with_background_color(true); + + auto update = [&]() { + board_view.set_board(&game.board()); + board_view.set_score(game.score()); + board_view.update(); + }; + + update(); + + auto start_a_new_game = [&]() { + game = Game(4, 4); + update(); + }; + + Vector undo_stack; + + board_view.on_move = [&](Game::Direction direction) { + undo_stack.append(game); + auto outcome = game.attempt_move(direction); + switch (outcome) { + case Game::MoveOutcome::OK: + if (undo_stack.size() >= 16) + undo_stack.take_first(); + update(); + break; + case Game::MoveOutcome::InvalidMove: + undo_stack.take_last(); + break; + case Game::MoveOutcome::Won: + update(); + GUI::MessageBox::show(window, + String::format("Score = %d in %zu turns", game.score(), game.turns()), + "You won!", + GUI::MessageBox::Type::Information); + start_a_new_game(); + break; + case Game::MoveOutcome::GameOver: + update(); + GUI::MessageBox::show(window, + String::format("Score = %d in %zu turns", game.score(), game.turns()), + "You lost!", + GUI::MessageBox::Type::Information); + start_a_new_game(); + break; + } + }; auto menubar = GUI::MenuBar::construct(); auto& app_menu = menubar->add_menu("2048"); app_menu.add_action(GUI::Action::create("New game", { Mod_None, Key_F2 }, [&](auto&) { - game.reset(); + start_a_new_game(); })); app_menu.add_action(GUI::CommonActions::make_undo_action([&](auto&) { - game.undo(); + if (undo_stack.is_empty()) + return; + game = undo_stack.take_last(); + update(); })); app_menu.add_separator(); app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {