mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 04:07:35 +00:00
Games: Add ColorLines
This commit is contained in:
parent
d987ddc0ee
commit
28bb3367cb
14 changed files with 1119 additions and 0 deletions
4
Base/res/apps/ColorLines.af
Normal file
4
Base/res/apps/ColorLines.af
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[App]
|
||||||
|
Name=Color Lines
|
||||||
|
Executable=/bin/ColorLines
|
||||||
|
Category=Games
|
BIN
Base/res/icons/16x16/app-colorlines.png
Normal file
BIN
Base/res/icons/16x16/app-colorlines.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 590 B |
BIN
Base/res/icons/32x32/app-colorlines.png
Normal file
BIN
Base/res/icons/32x32/app-colorlines.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
Base/res/icons/colorlines/colorlines.png
Normal file
BIN
Base/res/icons/colorlines/colorlines.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
19
Base/usr/share/man/man6/ColorLines.md
Normal file
19
Base/usr/share/man/man6/ColorLines.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
## Name
|
||||||
|
|
||||||
|
 Color Lines
|
||||||
|
|
||||||
|
[Open](file:///bin/ColorLines)
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
```**sh
|
||||||
|
$ ColorLines
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
ColorLines is a classic game.
|
||||||
|
|
||||||
|
Click a marble, then click an empty square to move.
|
||||||
|
You can only move along unblocked paths.
|
||||||
|
Build rows of 5 or more marbles of the same color to score.
|
|
@ -1,6 +1,7 @@
|
||||||
add_subdirectory(2048)
|
add_subdirectory(2048)
|
||||||
add_subdirectory(BrickGame)
|
add_subdirectory(BrickGame)
|
||||||
add_subdirectory(Chess)
|
add_subdirectory(Chess)
|
||||||
|
add_subdirectory(ColorLines)
|
||||||
add_subdirectory(FlappyBug)
|
add_subdirectory(FlappyBug)
|
||||||
add_subdirectory(Flood)
|
add_subdirectory(Flood)
|
||||||
add_subdirectory(GameOfLife)
|
add_subdirectory(GameOfLife)
|
||||||
|
|
13
Userland/Games/ColorLines/CMakeLists.txt
Normal file
13
Userland/Games/ColorLines/CMakeLists.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
serenity_component(
|
||||||
|
ColorLines
|
||||||
|
RECOMMENDED
|
||||||
|
TARGETS ColorLines
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
ColorLines.cpp
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
serenity_app(ColorLines ICON app-colorlines)
|
||||||
|
target_link_libraries(ColorLines PRIVATE LibGUI LibCore LibGfx LibConfig LibMain LibDesktop)
|
400
Userland/Games/ColorLines/ColorLines.cpp
Normal file
400
Userland/Games/ColorLines/ColorLines.cpp
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ColorLines.h"
|
||||||
|
#include "HueFilter.h"
|
||||||
|
#include "Marble.h"
|
||||||
|
#include "MarbleBoard.h"
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <LibConfig/Client.h>
|
||||||
|
#include <LibGUI/MessageBox.h>
|
||||||
|
#include <LibGUI/Painter.h>
|
||||||
|
#include <LibGfx/Font/Emoji.h>
|
||||||
|
|
||||||
|
ColorLines::BitmapArray ColorLines::build_marble_color_bitmaps()
|
||||||
|
{
|
||||||
|
auto marble_bitmap = MUST(Gfx::Bitmap::try_load_from_file("/res/icons/colorlines/colorlines.png"sv));
|
||||||
|
float constexpr hue_degrees[Marble::number_of_colors] = {
|
||||||
|
0, // Red
|
||||||
|
45, // Brown/Yellow
|
||||||
|
90, // Green
|
||||||
|
180, // Cyan
|
||||||
|
225, // Blue
|
||||||
|
300 // Purple
|
||||||
|
};
|
||||||
|
BitmapArray colored_bitmaps;
|
||||||
|
colored_bitmaps.ensure_capacity(Marble::number_of_colors);
|
||||||
|
for (int i = 0; i < Marble::number_of_colors; ++i) {
|
||||||
|
auto bitmap = MUST(marble_bitmap->clone());
|
||||||
|
HueFilter filter { hue_degrees[i] };
|
||||||
|
filter.apply(*bitmap, bitmap->rect(), *marble_bitmap, marble_bitmap->rect());
|
||||||
|
colored_bitmaps.append(bitmap);
|
||||||
|
}
|
||||||
|
return colored_bitmaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorLines::BitmapArray ColorLines::build_marble_trace_bitmaps()
|
||||||
|
{
|
||||||
|
// Use "Paw Prints" Unicode Character (U+1F43E)
|
||||||
|
auto trace_bitmap = NonnullRefPtr<Gfx::Bitmap>(*Gfx::Emoji::emoji_for_code_point(0x1F43E));
|
||||||
|
BitmapArray result;
|
||||||
|
result.ensure_capacity(number_of_marble_trace_bitmaps);
|
||||||
|
result.append(trace_bitmap);
|
||||||
|
result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
|
||||||
|
result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
|
||||||
|
result.append(MUST(result.last()->rotated(Gfx::RotationDirection::Clockwise)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorLines::ColorLines(StringView app_name)
|
||||||
|
: m_app_name { app_name }
|
||||||
|
, m_game_state { GameState::Idle }
|
||||||
|
, m_board { make<MarbleBoard>() }
|
||||||
|
, m_marble_bitmaps { build_marble_color_bitmaps() }
|
||||||
|
, m_trace_bitmaps { build_marble_trace_bitmaps() }
|
||||||
|
, m_score_font { Gfx::BitmapFont::load_from_file("/res/fonts/MarietaBold24.font") }
|
||||||
|
{
|
||||||
|
VERIFY(m_marble_bitmaps.size() == Marble::number_of_colors);
|
||||||
|
set_font(Gfx::FontDatabase::default_fixed_width_font().bold_variant());
|
||||||
|
m_high_score = Config::read_i32(m_app_name, m_app_name, "HighScore"sv, 0);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::reset()
|
||||||
|
{
|
||||||
|
set_game_state(GameState::StartingGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::mousedown_event(GUI::MouseEvent& event)
|
||||||
|
{
|
||||||
|
if (m_game_state != GameState::Idle && m_game_state != GameState::MarbleSelected)
|
||||||
|
return;
|
||||||
|
auto const event_position = event.position().translated(
|
||||||
|
-frame_inner_rect().x(),
|
||||||
|
-frame_inner_rect().y() - board_vertical_margin);
|
||||||
|
if (event_position.x() < 0 || event_position.y() < 0)
|
||||||
|
return;
|
||||||
|
auto const clicked_cell = Point { event_position.x() / board_cell_dimension.width(),
|
||||||
|
event_position.y() / board_cell_dimension.height() };
|
||||||
|
if (!MarbleBoard::in_bounds(clicked_cell))
|
||||||
|
return;
|
||||||
|
if (m_board->has_selected_marble()) {
|
||||||
|
auto const selected_cell = m_board->selected_marble().position();
|
||||||
|
if (selected_cell == clicked_cell) {
|
||||||
|
m_board->reset_selection();
|
||||||
|
set_game_state(GameState::Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_board->is_empty_cell_at(clicked_cell)) {
|
||||||
|
if (m_board->build_marble_path(selected_cell, clicked_cell, m_marble_path))
|
||||||
|
set_game_state(GameState::MarbleMoving);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_board->select_marble(clicked_cell))
|
||||||
|
set_game_state(GameState::MarbleSelected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_board->select_marble(clicked_cell))
|
||||||
|
set_game_state(GameState::MarbleSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::timer_event(Core::TimerEvent&)
|
||||||
|
{
|
||||||
|
switch (m_game_state) {
|
||||||
|
case GameState::GeneratingMarbles:
|
||||||
|
update();
|
||||||
|
if (--m_marble_animation_frame < AnimationFrames::marble_generating_end) {
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
set_game_state(GameState::CheckingMarbles);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameState::MarbleSelected:
|
||||||
|
m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames;
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameState::MarbleMoving:
|
||||||
|
m_marble_animation_frame = (m_marble_animation_frame + 1) % AnimationFrames::number_of_marble_bounce_frames;
|
||||||
|
update();
|
||||||
|
if (m_marble_path.remaining_steps() != 1 && m_marble_animation_frame != AnimationFrames::marble_at_top)
|
||||||
|
break;
|
||||||
|
if (auto const point = m_marble_path.next_point(); m_marble_path.is_empty()) {
|
||||||
|
auto const color = m_board->selected_marble().color();
|
||||||
|
m_board->reset_selection();
|
||||||
|
m_board->set_color_at(point, color);
|
||||||
|
if (m_board->check_and_remove_marbles())
|
||||||
|
set_game_state(GameState::MarblesRemoving);
|
||||||
|
else
|
||||||
|
set_game_state(GameState::GeneratingMarbles);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameState::MarblesRemoving:
|
||||||
|
update();
|
||||||
|
if (++m_marble_animation_frame > AnimationFrames::marble_removing_end) {
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
m_score += 2 * m_board->removed_marbles().size();
|
||||||
|
set_game_state(GameState::Idle);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameState::StartingGame:
|
||||||
|
case GameState::Idle:
|
||||||
|
case GameState::CheckingMarbles:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GameState::GameOver: {
|
||||||
|
stop_timer();
|
||||||
|
update();
|
||||||
|
StringBuilder text;
|
||||||
|
text.appendff("Your score is {}", m_score);
|
||||||
|
if (m_score > m_high_score) {
|
||||||
|
text.append("\nThis is a new high score!"sv);
|
||||||
|
Config::write_i32(m_app_name, m_app_name, "HighScore"sv, int(m_high_score = m_score));
|
||||||
|
}
|
||||||
|
GUI::MessageBox::show(window(),
|
||||||
|
text.string_view(),
|
||||||
|
"Game Over"sv,
|
||||||
|
GUI::MessageBox::Type::Information);
|
||||||
|
reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::paint_event(GUI::PaintEvent& event)
|
||||||
|
{
|
||||||
|
GUI::Frame::paint_event(event);
|
||||||
|
GUI::Painter painter(*this);
|
||||||
|
painter.add_clip_rect(frame_inner_rect());
|
||||||
|
painter.add_clip_rect(event.rect());
|
||||||
|
|
||||||
|
auto paint_cell = [&](GUI::Painter& painter, Gfx::IntRect rect, int color, int animation_frame) {
|
||||||
|
painter.draw_rect(rect, Color::Black);
|
||||||
|
rect.shrink(0, 1, 1, 0);
|
||||||
|
painter.draw_line(rect.bottom_left(), rect.top_left(), Color::White);
|
||||||
|
painter.draw_line(rect.top_left(), rect.top_right(), Color::White);
|
||||||
|
painter.draw_line(rect.top_right(), rect.bottom_right(), Color::DarkGray);
|
||||||
|
painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::DarkGray);
|
||||||
|
rect.shrink(1, 1, 1, 1);
|
||||||
|
painter.draw_line(rect.bottom_left(), rect.top_left(), Color::LightGray);
|
||||||
|
painter.draw_line(rect.top_left(), rect.top_right(), Color::LightGray);
|
||||||
|
painter.draw_line(rect.top_right(), rect.bottom_right(), Color::MidGray);
|
||||||
|
painter.draw_line(rect.bottom_right(), rect.bottom_left(), Color::MidGray);
|
||||||
|
rect.shrink(1, 1, 1, 1);
|
||||||
|
painter.fill_rect(rect, tile_color);
|
||||||
|
rect.shrink(1, 1, 1, 1);
|
||||||
|
if (color >= 0 && color < Marble::number_of_colors) {
|
||||||
|
auto const source_rect = Gfx::IntRect { animation_frame * marble_pixel_size, 0, marble_pixel_size, marble_pixel_size };
|
||||||
|
painter.draw_scaled_bitmap(rect, *m_marble_bitmaps[color], source_rect,
|
||||||
|
1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
painter.set_font(*m_score_font);
|
||||||
|
|
||||||
|
// Draw board header with score, high score
|
||||||
|
auto board_header_size = frame_inner_rect().size();
|
||||||
|
board_header_size.set_height(board_vertical_margin);
|
||||||
|
auto const board_header_rect = Gfx::IntRect { frame_inner_rect().top_left(), board_header_size };
|
||||||
|
painter.fill_rect(board_header_rect, Color::Black);
|
||||||
|
|
||||||
|
auto const text_margin = 8;
|
||||||
|
|
||||||
|
// Draw score
|
||||||
|
auto const score_text = MUST(String::formatted("{:05}"sv, m_score));
|
||||||
|
auto text_width { m_score_font->width(score_text) };
|
||||||
|
auto const glyph_height = m_score_font->glyph_height();
|
||||||
|
auto const score_text_rect = Gfx::IntRect {
|
||||||
|
frame_inner_rect().top_left().translated(text_margin),
|
||||||
|
Gfx::IntSize { text_width, glyph_height }
|
||||||
|
};
|
||||||
|
painter.draw_text(score_text_rect, score_text, Gfx::TextAlignment::CenterLeft, text_color);
|
||||||
|
|
||||||
|
// Draw high score
|
||||||
|
auto const high_score_text = MUST(String::formatted("{:05}"sv, m_high_score));
|
||||||
|
text_width = m_score_font->width(high_score_text);
|
||||||
|
auto const high_score_text_rect = Gfx::IntRect {
|
||||||
|
frame_inner_rect().top_right().translated(-(text_margin + text_width), text_margin),
|
||||||
|
Gfx::IntSize { text_width, glyph_height }
|
||||||
|
};
|
||||||
|
painter.draw_text(high_score_text_rect, high_score_text, Gfx::TextAlignment::CenterLeft, text_color);
|
||||||
|
|
||||||
|
auto const cell_rect
|
||||||
|
= Gfx::IntRect(frame_inner_rect().top_left(), board_cell_dimension)
|
||||||
|
.translated(0, board_vertical_margin);
|
||||||
|
|
||||||
|
// Draw all cells and the selected marble if it exists
|
||||||
|
for (int y = 0; y < MarbleBoard::board_size.height(); ++y)
|
||||||
|
for (int x = 0; x < MarbleBoard::board_size.width(); ++x) {
|
||||||
|
auto const& destination_rect = cell_rect.translated(
|
||||||
|
x * board_cell_dimension.width(),
|
||||||
|
y * board_cell_dimension.height());
|
||||||
|
auto const point = Point { x, y };
|
||||||
|
auto const animation_frame = m_game_state == GameState::MarbleSelected && m_board->has_selected_marble()
|
||||||
|
&& m_board->selected_marble().position() == point
|
||||||
|
? m_marble_animation_frame
|
||||||
|
: AnimationFrames::marble_default;
|
||||||
|
paint_cell(painter, destination_rect, m_board->color_at(point), animation_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw preview marbles in the board
|
||||||
|
for (auto const& marble : m_board->preview_marbles()) {
|
||||||
|
auto const& point = marble.position();
|
||||||
|
if (m_marble_path.contains(point) || !m_board->is_empty_cell_at(point))
|
||||||
|
continue;
|
||||||
|
auto const& destination_rect = cell_rect.translated(
|
||||||
|
point.x() * board_cell_dimension.width(),
|
||||||
|
point.y() * board_cell_dimension.height());
|
||||||
|
auto get_animation_frame = [this]() -> int {
|
||||||
|
switch (m_game_state) {
|
||||||
|
case GameState::GameOver:
|
||||||
|
return AnimationFrames::marble_default;
|
||||||
|
case GameState::GeneratingMarbles:
|
||||||
|
case GameState::CheckingMarbles:
|
||||||
|
return m_marble_animation_frame;
|
||||||
|
default:
|
||||||
|
return AnimationFrames::marble_generating_start;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
paint_cell(painter, destination_rect, marble.color(), get_animation_frame());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw preview marbles in the board header
|
||||||
|
for (size_t i = 0; i < MarbleBoard::number_of_preview_marbles; ++i) {
|
||||||
|
auto const& marble = m_board->preview_marbles()[i];
|
||||||
|
auto const& destination_rect = cell_rect.translated(
|
||||||
|
int(i + 3) * board_cell_dimension.width(),
|
||||||
|
-board_vertical_margin)
|
||||||
|
.shrunken(10, 10);
|
||||||
|
paint_cell(painter, destination_rect, marble.color(), AnimationFrames::marble_preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw moving marble
|
||||||
|
if (!m_marble_path.is_empty()) {
|
||||||
|
auto const point = m_marble_path.current_point();
|
||||||
|
auto const& destination_rect = cell_rect.translated(
|
||||||
|
point.x() * board_cell_dimension.width(),
|
||||||
|
point.y() * board_cell_dimension.height());
|
||||||
|
paint_cell(painter, destination_rect, m_board->selected_marble().color(), m_marble_animation_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw removing marble
|
||||||
|
if (m_game_state == GameState::MarblesRemoving)
|
||||||
|
for (auto const& marble : m_board->removed_marbles()) {
|
||||||
|
auto const& point = marble.position();
|
||||||
|
auto const& destination_rect = cell_rect.translated(
|
||||||
|
point.x() * board_cell_dimension.width(),
|
||||||
|
point.y() * board_cell_dimension.height());
|
||||||
|
paint_cell(painter, destination_rect, marble.color(), m_marble_animation_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw marble move trace
|
||||||
|
if (m_game_state == GameState::MarbleMoving && m_marble_path.remaining_steps() > 1) {
|
||||||
|
auto const trace_size = Gfx::IntSize { m_trace_bitmaps.first()->width(), m_trace_bitmaps.first()->height() };
|
||||||
|
auto const target_trace_size = Gfx::IntSize { 14, 14 };
|
||||||
|
auto const source_rect = Gfx::FloatRect(Gfx::IntPoint {}, trace_size);
|
||||||
|
for (size_t i = 0; i < m_marble_path.remaining_steps() - 1; ++i) {
|
||||||
|
auto const& current_step = m_marble_path[i];
|
||||||
|
auto const destination_rect = Gfx::IntRect(frame_inner_rect().top_left(), target_trace_size)
|
||||||
|
.translated(
|
||||||
|
current_step.x() * board_cell_dimension.width(),
|
||||||
|
board_vertical_margin + current_step.y() * board_cell_dimension.height())
|
||||||
|
.translated(
|
||||||
|
(board_cell_dimension.width() - target_trace_size.width()) / 2,
|
||||||
|
(board_cell_dimension.height() - target_trace_size.height()) / 2);
|
||||||
|
auto get_direction_bitmap_index = [&]() -> size_t {
|
||||||
|
auto const& previous_step = m_marble_path[i + 1];
|
||||||
|
if (previous_step.x() > current_step.x())
|
||||||
|
return 3;
|
||||||
|
if (previous_step.x() < current_step.x())
|
||||||
|
return 1;
|
||||||
|
if (previous_step.y() > current_step.y())
|
||||||
|
return 0;
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
painter.draw_scaled_bitmap(destination_rect, *m_trace_bitmaps[get_direction_bitmap_index()], source_rect,
|
||||||
|
1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::restart_timer(int milliseconds)
|
||||||
|
{
|
||||||
|
stop_timer();
|
||||||
|
start_timer(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLines::set_game_state(GameState state)
|
||||||
|
{
|
||||||
|
m_game_state = state;
|
||||||
|
switch (state) {
|
||||||
|
case GameState::StartingGame:
|
||||||
|
m_marble_path.reset();
|
||||||
|
m_board->reset();
|
||||||
|
m_score = 0;
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
update();
|
||||||
|
if (m_board->update_preview_marbles(false))
|
||||||
|
set_game_state(GameState::GeneratingMarbles);
|
||||||
|
else
|
||||||
|
set_game_state(GameState::GameOver);
|
||||||
|
break;
|
||||||
|
case GameState::GeneratingMarbles:
|
||||||
|
m_board->reset_selection();
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_generating_start;
|
||||||
|
update();
|
||||||
|
if (m_board->ensure_all_preview_marbles_are_on_empty_cells())
|
||||||
|
restart_timer(TimerIntervals::generating_marbles);
|
||||||
|
else
|
||||||
|
set_game_state(GameState::GameOver);
|
||||||
|
break;
|
||||||
|
case GameState::MarblesRemoving:
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_removing_start;
|
||||||
|
update();
|
||||||
|
restart_timer(TimerIntervals::removing_marbles);
|
||||||
|
break;
|
||||||
|
case GameState::Idle:
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
update();
|
||||||
|
if (m_board->ensure_all_preview_marbles_are_on_empty_cells() && m_board->has_empty_cells())
|
||||||
|
stop_timer();
|
||||||
|
else
|
||||||
|
set_game_state(GameState::GameOver);
|
||||||
|
break;
|
||||||
|
case GameState::MarbleSelected:
|
||||||
|
restart_timer(TimerIntervals::selected_marble);
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case GameState::CheckingMarbles:
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
update();
|
||||||
|
if (!m_board->place_preview_marbles_on_board())
|
||||||
|
set_game_state(GameState::GameOver);
|
||||||
|
else if (m_board->check_and_remove_marbles())
|
||||||
|
set_game_state(GameState::MarblesRemoving);
|
||||||
|
else
|
||||||
|
set_game_state(GameState::Idle);
|
||||||
|
break;
|
||||||
|
case GameState::MarbleMoving:
|
||||||
|
restart_timer(TimerIntervals::moving_marble);
|
||||||
|
m_board->clear_color_at(m_board->selected_marble().position());
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
case GameState::GameOver:
|
||||||
|
m_marble_animation_frame = AnimationFrames::marble_default;
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
}
|
90
Userland/Games/ColorLines/ColorLines.h
Normal file
90
Userland/Games/ColorLines/ColorLines.h
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MarblePath.h"
|
||||||
|
#include <AK/NonnullRefPtr.h>
|
||||||
|
#include <AK/RefPtr.h>
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibGUI/Frame.h>
|
||||||
|
#include <LibGfx/Font/BitmapFont.h>
|
||||||
|
#include <LibGfx/Forward.h>
|
||||||
|
|
||||||
|
class MarbleBoard;
|
||||||
|
|
||||||
|
class ColorLines : public GUI::Frame {
|
||||||
|
C_OBJECT(ColorLines);
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual ~ColorLines() override = default;
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class GameState {
|
||||||
|
Idle = 0, // No marble is selected, waiting for marble selection
|
||||||
|
StartingGame, // Game is starting
|
||||||
|
GeneratingMarbles, // Three new marbles are being generated
|
||||||
|
MarbleSelected, // Marble is selected, waiting for the target cell selection
|
||||||
|
MarbleMoving, // Selected marble is moving to the target cell
|
||||||
|
MarblesRemoving, // Selected marble has completed the move and some marbles are being removed from the board
|
||||||
|
CheckingMarbles, // Checking whether marbles on the board form lines of 5 or more marbles
|
||||||
|
GameOver // Game is over
|
||||||
|
};
|
||||||
|
|
||||||
|
ColorLines(StringView app_name);
|
||||||
|
|
||||||
|
virtual void paint_event(GUI::PaintEvent&) override;
|
||||||
|
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||||
|
virtual void timer_event(Core::TimerEvent&) override;
|
||||||
|
|
||||||
|
void set_game_state(GameState state);
|
||||||
|
void restart_timer(int milliseconds);
|
||||||
|
|
||||||
|
using Point = Gfx::IntPoint;
|
||||||
|
using BitmapArray = Vector<NonnullRefPtr<Gfx::Bitmap>>;
|
||||||
|
|
||||||
|
StringView const m_app_name;
|
||||||
|
GameState m_game_state { GameState::Idle };
|
||||||
|
NonnullOwnPtr<MarbleBoard> m_board;
|
||||||
|
BitmapArray const m_marble_bitmaps;
|
||||||
|
BitmapArray const m_trace_bitmaps;
|
||||||
|
RefPtr<Gfx::BitmapFont> m_score_font;
|
||||||
|
MarblePath m_marble_path {};
|
||||||
|
int m_marble_animation_frame {};
|
||||||
|
unsigned m_score {};
|
||||||
|
unsigned m_high_score {};
|
||||||
|
|
||||||
|
static BitmapArray build_marble_color_bitmaps();
|
||||||
|
static BitmapArray build_marble_trace_bitmaps();
|
||||||
|
|
||||||
|
static constexpr auto marble_pixel_size { 40 };
|
||||||
|
static constexpr auto board_vertical_margin { 45 };
|
||||||
|
static constexpr auto board_cell_dimension = Gfx::IntSize { 48, 48 };
|
||||||
|
static constexpr auto number_of_marble_trace_bitmaps { 4 };
|
||||||
|
static constexpr auto tile_color { Color::from_rgb(0xc0c0c0) };
|
||||||
|
static constexpr auto text_color { Color::from_rgb(0x00a0ff) };
|
||||||
|
|
||||||
|
enum AnimationFrames {
|
||||||
|
marble_default = 0,
|
||||||
|
marble_at_top = 2,
|
||||||
|
marble_preview = 18,
|
||||||
|
marble_generating_start = 21,
|
||||||
|
marble_generating_end = 17,
|
||||||
|
marble_removing_start = 7,
|
||||||
|
marble_removing_end = 16,
|
||||||
|
number_of_marble_bounce_frames = 7
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TimerIntervals {
|
||||||
|
generating_marbles = 80,
|
||||||
|
removing_marbles = 60,
|
||||||
|
selected_marble = 70,
|
||||||
|
moving_marble = 28
|
||||||
|
};
|
||||||
|
};
|
51
Userland/Games/ColorLines/HueFilter.h
Normal file
51
Userland/Games/ColorLines/HueFilter.h
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Math.h>
|
||||||
|
#include <LibGfx/Filters/MatrixFilter.h>
|
||||||
|
|
||||||
|
// This filter is similar to LibGfx/Filters/HueRotateFilter.h, however it uses
|
||||||
|
// a different formula (matrix) for hue rotation. This filter provides brighter
|
||||||
|
// colors compared to the filter provided in LibGfx.
|
||||||
|
class HueFilter : public Gfx::MatrixFilter {
|
||||||
|
public:
|
||||||
|
HueFilter(float angle_degrees)
|
||||||
|
: Gfx::MatrixFilter(calculate_hue_rotate_matrix(angle_degrees))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool amount_handled_in_filter() const override
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual StringView class_name() const override { return "HueFilter"sv; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static FloatMatrix3x3 calculate_hue_rotate_matrix(float angle_degrees)
|
||||||
|
{
|
||||||
|
float const angle_rads = angle_degrees * (AK::Pi<float> / 180.0f);
|
||||||
|
float cos_angle = 0.;
|
||||||
|
float sin_angle = 0.;
|
||||||
|
AK::sincos(angle_rads, sin_angle, cos_angle);
|
||||||
|
return FloatMatrix3x3 {
|
||||||
|
float(cos_angle + (1.0f - cos_angle) / 3.0f),
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
float(cos_angle + 1.0f / 3.0f * (1.0f - cos_angle)),
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) - sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
float(1.0f / 3.0f * (1.0f - cos_angle) + sqrtf(1.0f / 3.0f) * sin_angle),
|
||||||
|
float(cos_angle + 1.0f / 3.0f * (1.0f - cos_angle))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
46
Userland/Games/ColorLines/Marble.h
Normal file
46
Userland/Games/ColorLines/Marble.h
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibGfx/Point.h>
|
||||||
|
|
||||||
|
class Marble final {
|
||||||
|
public:
|
||||||
|
using Point = Gfx::IntPoint;
|
||||||
|
using Color = u8;
|
||||||
|
|
||||||
|
static constexpr int number_of_colors { 6 };
|
||||||
|
static constexpr Color empty_cell = NumericLimits<Color>::max();
|
||||||
|
|
||||||
|
Marble() = default;
|
||||||
|
Marble(Point position, Color color)
|
||||||
|
: m_position { position }
|
||||||
|
, m_color { color }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(Marble const& other) const = default;
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Point position() const { return m_position; }
|
||||||
|
|
||||||
|
[[nodiscard]] constexpr Color color() const { return m_color; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Point m_position {};
|
||||||
|
Color m_color {};
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace AK {
|
||||||
|
template<>
|
||||||
|
struct Traits<Marble> : public GenericTraits<Marble> {
|
||||||
|
static unsigned hash(Marble const& marble)
|
||||||
|
{
|
||||||
|
return Traits<Marble::Point>::hash(marble.position());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
356
Userland/Games/ColorLines/MarbleBoard.h
Normal file
356
Userland/Games/ColorLines/MarbleBoard.h
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Marble.h"
|
||||||
|
#include "MarblePath.h"
|
||||||
|
#include <AK/Array.h>
|
||||||
|
#include <AK/Function.h>
|
||||||
|
#include <AK/HashTable.h>
|
||||||
|
#include <AK/IterationDecision.h>
|
||||||
|
#include <AK/NumericLimits.h>
|
||||||
|
#include <AK/Queue.h>
|
||||||
|
#include <AK/Random.h>
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibGfx/Point.h>
|
||||||
|
#include <LibGfx/Size.h>
|
||||||
|
|
||||||
|
class MarbleBoard final {
|
||||||
|
public:
|
||||||
|
using Color = Marble::Color;
|
||||||
|
using Point = Gfx::IntPoint;
|
||||||
|
using PointArray = Vector<Point>;
|
||||||
|
using SelectedMarble = Marble;
|
||||||
|
using PreviewMarble = Marble;
|
||||||
|
using MarbleArray = Vector<Marble>;
|
||||||
|
|
||||||
|
static constexpr Gfx::IntSize board_size { 9, 9 };
|
||||||
|
static constexpr size_t number_of_preview_marbles = 3;
|
||||||
|
static constexpr Color empty_cell = Marble::empty_cell;
|
||||||
|
|
||||||
|
using PreviewMarbles = Array<PreviewMarble, number_of_preview_marbles>;
|
||||||
|
|
||||||
|
MarbleBoard()
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
~MarbleBoard() = default;
|
||||||
|
|
||||||
|
MarbleBoard(MarbleBoard const&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] bool has_empty_cells() const
|
||||||
|
{
|
||||||
|
bool result = false;
|
||||||
|
for_each_cell([&](Point point) {
|
||||||
|
result = is_empty_cell_at(point);
|
||||||
|
return result ? IterationDecision::Break : IterationDecision::Continue;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] PointArray get_empty_cells() const
|
||||||
|
{
|
||||||
|
PointArray result;
|
||||||
|
for_each_cell([&](Point point) {
|
||||||
|
if (is_empty_cell_at(point))
|
||||||
|
result.append(point);
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
});
|
||||||
|
random_shuffle(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_preview_marble(size_t i, PreviewMarble const& marble)
|
||||||
|
{
|
||||||
|
VERIFY(i < number_of_preview_marbles);
|
||||||
|
m_preview_marbles[i] = marble;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool place_preview_marbles_on_board()
|
||||||
|
{
|
||||||
|
if (!ensure_all_preview_marbles_are_on_empty_cells())
|
||||||
|
return false;
|
||||||
|
for (auto const& marble : m_preview_marbles)
|
||||||
|
if (!place_preview_marble_on_board(marble))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool check_preview_marbles_are_valid()
|
||||||
|
{
|
||||||
|
// Check marbles pairwise and also check the board cell under this marble is empty
|
||||||
|
static_assert(number_of_preview_marbles == 3);
|
||||||
|
return m_preview_marbles[0].position() != m_preview_marbles[1].position() && m_preview_marbles[0].position() != m_preview_marbles[2].position()
|
||||||
|
&& m_preview_marbles[1].position() != m_preview_marbles[2].position()
|
||||||
|
&& is_empty_cell_at(m_preview_marbles[0].position())
|
||||||
|
&& is_empty_cell_at(m_preview_marbles[1].position())
|
||||||
|
&& is_empty_cell_at(m_preview_marbles[2].position());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool update_preview_marbles(bool use_current)
|
||||||
|
{
|
||||||
|
auto empty_cells = get_empty_cells();
|
||||||
|
for (size_t i = 0; i < number_of_preview_marbles; ++i) {
|
||||||
|
auto marble = m_preview_marbles[i];
|
||||||
|
// Check marbles pairwise and also check the board cell under this marble is empty
|
||||||
|
auto const is_valid_marble = [&]() {
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
return marble.position() != m_preview_marbles[1].position() && marble.position() != m_preview_marbles[2].position() && is_empty_cell_at(marble.position());
|
||||||
|
case 1:
|
||||||
|
return marble.position() != m_preview_marbles[0].position() && marble.position() != m_preview_marbles[2].position() && is_empty_cell_at(marble.position());
|
||||||
|
case 2:
|
||||||
|
return marble.position() != m_preview_marbles[0].position() && marble.position() != m_preview_marbles[1].position() && is_empty_cell_at(marble.position());
|
||||||
|
default:
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (use_current && is_valid_marble()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (!empty_cells.is_empty()) {
|
||||||
|
auto const position = empty_cells.take_last();
|
||||||
|
Color const new_color = get_random_uniform(Marble::number_of_colors);
|
||||||
|
marble = Marble { position, new_color };
|
||||||
|
if (!is_valid_marble())
|
||||||
|
continue;
|
||||||
|
set_preview_marble(i, marble);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (empty_cells.is_empty())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return empty_cells.size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool ensure_all_preview_marbles_are_on_empty_cells()
|
||||||
|
{
|
||||||
|
if (check_preview_marbles_are_valid())
|
||||||
|
return true;
|
||||||
|
return update_preview_marbles(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Color color_at(Point point) const
|
||||||
|
{
|
||||||
|
VERIFY(in_bounds(point));
|
||||||
|
return m_board[point.y()][point.x()];
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_color_at(Point point, Color color)
|
||||||
|
{
|
||||||
|
VERIFY(in_bounds(point));
|
||||||
|
m_board[point.y()][point.x()] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_color_at(Point point)
|
||||||
|
{
|
||||||
|
set_color_at(point, empty_cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_empty_cell_at(Point point) const
|
||||||
|
{
|
||||||
|
return color_at(point) == empty_cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] static bool in_bounds(Point point)
|
||||||
|
{
|
||||||
|
return point.x() >= 0 && point.x() < board_size.width() && point.y() >= 0 && point.y() < board_size.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool build_marble_path(Point from, Point to, MarblePath& path) const
|
||||||
|
{
|
||||||
|
path.reset();
|
||||||
|
|
||||||
|
if (from == to || !MarbleBoard::in_bounds(from) || !MarbleBoard::in_bounds(to)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Trace {
|
||||||
|
public:
|
||||||
|
using Value = u8;
|
||||||
|
|
||||||
|
Trace() { reset(); }
|
||||||
|
|
||||||
|
~Trace() = default;
|
||||||
|
|
||||||
|
[[nodiscard]] Value operator[](Point point) const
|
||||||
|
{
|
||||||
|
return m_map[point.y()][point.x()];
|
||||||
|
}
|
||||||
|
|
||||||
|
Value& operator[](Point point)
|
||||||
|
{
|
||||||
|
return m_map[point.y()][point.x()];
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
for (size_t y = 0; y < board_size.height(); ++y)
|
||||||
|
for (size_t x = 0; x < board_size.width(); ++x)
|
||||||
|
m_map[y][x] = NumericLimits<Value>::max();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
BoardMap m_map;
|
||||||
|
};
|
||||||
|
|
||||||
|
Trace trace;
|
||||||
|
trace[from] = 1;
|
||||||
|
|
||||||
|
Queue<Point> queue;
|
||||||
|
queue.enqueue(from);
|
||||||
|
|
||||||
|
auto add_path_point = [&](Point point, u8 value) {
|
||||||
|
if (MarbleBoard::in_bounds(point) && is_empty_cell_at(point) && trace[point] > value) {
|
||||||
|
trace[point] = value;
|
||||||
|
queue.enqueue(point);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Point connected_four_ways[4] = {
|
||||||
|
{ 0, -1 }, // to the top
|
||||||
|
{ 0, 1 }, // to the bottom
|
||||||
|
{ -1, 0 }, // to the left
|
||||||
|
{ 1, 0 } // to the right
|
||||||
|
};
|
||||||
|
|
||||||
|
while (!queue.is_empty()) {
|
||||||
|
auto current = queue.dequeue();
|
||||||
|
if (current == to) {
|
||||||
|
while (current != from) {
|
||||||
|
path.add_point(current);
|
||||||
|
for (auto delta : connected_four_ways)
|
||||||
|
if (auto next = current.translated(delta); MarbleBoard::in_bounds(next) && trace[next] < trace[current]) {
|
||||||
|
current = next;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.add_point(current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (auto delta : connected_four_ways)
|
||||||
|
add_path_point(current.translated(delta), trace[current] + 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool check_and_remove_marbles()
|
||||||
|
{
|
||||||
|
m_removed_marbles.clear();
|
||||||
|
constexpr Point connected_four_ways[] = {
|
||||||
|
{ -1, 0 }, // to the left
|
||||||
|
{ 0, -1 }, // to the top
|
||||||
|
{ -1, -1 }, // to the top-left
|
||||||
|
{ 1, -1 } // to the top-right
|
||||||
|
};
|
||||||
|
HashTable<Marble, Traits<Marble>> marbles;
|
||||||
|
for_each_cell([&](Point current_point) {
|
||||||
|
if (is_empty_cell_at(current_point))
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
auto const color { color_at(current_point) };
|
||||||
|
for (auto direction : connected_four_ways) {
|
||||||
|
size_t marble_count = 0;
|
||||||
|
for (auto p = current_point; in_bounds(p) && color_at(p) == color; p.translate_by(direction))
|
||||||
|
++marble_count;
|
||||||
|
if (marble_count >= number_of_marbles_to_remove)
|
||||||
|
for (auto p = current_point; in_bounds(p) && color_at(p) == color; p.translate_by(direction))
|
||||||
|
marbles.set({ p, color });
|
||||||
|
}
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
});
|
||||||
|
m_removed_marbles.ensure_capacity(marbles.size());
|
||||||
|
for (auto const& marble : marbles) {
|
||||||
|
m_removed_marbles.append(marble);
|
||||||
|
clear_color_at(marble.position());
|
||||||
|
}
|
||||||
|
return !m_removed_marbles.is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] PreviewMarbles const& preview_marbles() const
|
||||||
|
{
|
||||||
|
return m_preview_marbles;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool has_selected_marble() const
|
||||||
|
{
|
||||||
|
return m_selected_marble != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] SelectedMarble const& selected_marble() const
|
||||||
|
{
|
||||||
|
VERIFY(has_selected_marble());
|
||||||
|
return *m_selected_marble;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool select_marble(Point point)
|
||||||
|
{
|
||||||
|
if (!is_empty_cell_at(point)) {
|
||||||
|
m_selected_marble = make<SelectedMarble>(point, color_at(point));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset_selection()
|
||||||
|
{
|
||||||
|
m_selected_marble.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] MarbleArray const& removed_marbles() const
|
||||||
|
{
|
||||||
|
return m_removed_marbles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
reset_selection();
|
||||||
|
for (size_t i = 0; i < number_of_preview_marbles; ++i)
|
||||||
|
m_preview_marbles[i] = { { 0, 0 }, empty_cell };
|
||||||
|
m_removed_marbles.clear();
|
||||||
|
for_each_cell([&](Point point) {
|
||||||
|
set_color_at(point, empty_cell);
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void for_each_cell(Function<IterationDecision(Point)> functor)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < board_size.height(); ++y)
|
||||||
|
for (int x = 0; x < board_size.width(); ++x)
|
||||||
|
if (functor({ x, y }) == IterationDecision::Break)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool place_preview_marble_on_board(PreviewMarble const& marble)
|
||||||
|
{
|
||||||
|
if (!is_empty_cell_at(marble.position()))
|
||||||
|
return false;
|
||||||
|
set_color_at(marble.position(), marble.color());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void random_shuffle(PointArray& points)
|
||||||
|
{
|
||||||
|
// Using Fisher–Yates in-place shuffle
|
||||||
|
if (points.size() > 1)
|
||||||
|
for (size_t i = points.size() - 1; i > 1; --i)
|
||||||
|
swap(points[i], points[get_random_uniform(i + 1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int number_of_marbles_to_remove { 5 };
|
||||||
|
|
||||||
|
using Row = Array<Color, board_size.width()>;
|
||||||
|
using BoardMap = Array<Row, board_size.height()>;
|
||||||
|
|
||||||
|
BoardMap m_board;
|
||||||
|
PreviewMarbles m_preview_marbles;
|
||||||
|
MarbleArray m_removed_marbles;
|
||||||
|
OwnPtr<SelectedMarble> m_selected_marble {};
|
||||||
|
};
|
64
Userland/Games/ColorLines/MarblePath.h
Normal file
64
Userland/Games/ColorLines/MarblePath.h
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Vector.h>
|
||||||
|
#include <LibGfx/Point.h>
|
||||||
|
|
||||||
|
class MarblePath final {
|
||||||
|
public:
|
||||||
|
using Point = Gfx::IntPoint;
|
||||||
|
|
||||||
|
MarblePath() = default;
|
||||||
|
|
||||||
|
void add_point(Point point)
|
||||||
|
{
|
||||||
|
m_path.append(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_empty() const
|
||||||
|
{
|
||||||
|
return m_path.is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool contains(Point point) const
|
||||||
|
{
|
||||||
|
return m_path.contains_slow(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] size_t remaining_steps() const
|
||||||
|
{
|
||||||
|
return m_path.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Point current_point() const
|
||||||
|
{
|
||||||
|
VERIFY(!m_path.is_empty());
|
||||||
|
return m_path.last();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Point next_point()
|
||||||
|
{
|
||||||
|
auto const point = current_point();
|
||||||
|
m_path.resize(m_path.size() - 1);
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Point operator[](size_t index) const
|
||||||
|
{
|
||||||
|
return m_path[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
m_path.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Vector<Point> m_path;
|
||||||
|
};
|
75
Userland/Games/ColorLines/main.cpp
Normal file
75
Userland/Games/ColorLines/main.cpp
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022, Oleg Kosenkov <oleg@kosenkov.ca>
|
||||||
|
* Copyright (c) 2022, the SerenityOS developers.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ColorLines.h"
|
||||||
|
#include <AK/URL.h>
|
||||||
|
#include <LibConfig/Client.h>
|
||||||
|
#include <LibCore/System.h>
|
||||||
|
#include <LibDesktop/Launcher.h>
|
||||||
|
#include <LibGUI/Action.h>
|
||||||
|
#include <LibGUI/Application.h>
|
||||||
|
#include <LibGUI/Icon.h>
|
||||||
|
#include <LibGUI/Menu.h>
|
||||||
|
#include <LibGUI/Menubar.h>
|
||||||
|
#include <LibGUI/Window.h>
|
||||||
|
#include <LibMain/Main.h>
|
||||||
|
|
||||||
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
|
{
|
||||||
|
TRY(Core::System::pledge("stdio rpath recvfd sendfd unix"));
|
||||||
|
|
||||||
|
auto app = TRY(GUI::Application::try_create(arguments));
|
||||||
|
|
||||||
|
auto const app_name = "ColorLines"sv;
|
||||||
|
auto const title = "Color Lines"sv;
|
||||||
|
auto const man_file = "/usr/share/man/man6/ColorLines.md"sv;
|
||||||
|
|
||||||
|
Config::pledge_domain(app_name);
|
||||||
|
|
||||||
|
TRY(Desktop::Launcher::add_allowed_handler_with_only_specific_urls("/bin/Help", { URL::create_with_file_scheme(man_file) }));
|
||||||
|
TRY(Desktop::Launcher::seal_allowlist());
|
||||||
|
|
||||||
|
TRY(Core::System::pledge("stdio rpath recvfd sendfd"));
|
||||||
|
|
||||||
|
TRY(Core::System::unveil("/tmp/session/%sid/portal/launch", "rw"));
|
||||||
|
TRY(Core::System::unveil("/res", "r"));
|
||||||
|
TRY(Core::System::unveil(nullptr, nullptr));
|
||||||
|
|
||||||
|
auto app_icon = TRY(GUI::Icon::try_create_default_icon("app-colorlines"sv));
|
||||||
|
|
||||||
|
auto window = TRY(GUI::Window::try_create());
|
||||||
|
|
||||||
|
window->set_double_buffering_enabled(false);
|
||||||
|
window->set_title(title);
|
||||||
|
window->resize(436, 481);
|
||||||
|
window->set_resizable(false);
|
||||||
|
|
||||||
|
auto game = TRY(window->try_set_main_widget<ColorLines>(app_name));
|
||||||
|
|
||||||
|
auto game_menu = TRY(window->try_add_menu("&Game"));
|
||||||
|
|
||||||
|
TRY(game_menu->try_add_action(GUI::Action::create("&New Game", { Mod_None, Key_F2 }, TRY(Gfx::Bitmap::try_load_from_file("/res/icons/16x16/reload.png"sv)), [&](auto&) {
|
||||||
|
game->reset();
|
||||||
|
})));
|
||||||
|
TRY(game_menu->try_add_separator());
|
||||||
|
TRY(game_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) {
|
||||||
|
GUI::Application::the()->quit();
|
||||||
|
})));
|
||||||
|
|
||||||
|
auto help_menu = TRY(window->try_add_menu("&Help"));
|
||||||
|
TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(window)));
|
||||||
|
TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([&man_file](auto&) {
|
||||||
|
Desktop::Launcher::open(URL::create_with_file_scheme(man_file), "/bin/Help");
|
||||||
|
})));
|
||||||
|
TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action(title, app_icon, window)));
|
||||||
|
|
||||||
|
window->show();
|
||||||
|
|
||||||
|
window->set_icon(app_icon.bitmap_for_size(16));
|
||||||
|
|
||||||
|
return app->exec();
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue