mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 04:17:35 +00:00
Games: Add ColorLines
This commit is contained in:
parent
d987ddc0ee
commit
28bb3367cb
14 changed files with 1119 additions and 0 deletions
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 {};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue