updates
This commit is contained in:
332
src/board_widget.cpp
Normal file
332
src/board_widget.cpp
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
#include "board_widget.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
BoardWidget::BoardWidget() {
|
||||||
|
set_hexpand(true);
|
||||||
|
set_vexpand(true);
|
||||||
|
|
||||||
|
// Setup Draw Function
|
||||||
|
set_draw_func(sigc::mem_fun(*this, &BoardWidget::on_draw));
|
||||||
|
|
||||||
|
// Left Click
|
||||||
|
left_click_controller_ = Gtk::GestureClick::create();
|
||||||
|
left_click_controller_->set_button(GDK_BUTTON_PRIMARY);
|
||||||
|
left_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_click_pressed));
|
||||||
|
add_controller(left_click_controller_);
|
||||||
|
|
||||||
|
// Right Click
|
||||||
|
right_click_controller_ = Gtk::GestureClick::create();
|
||||||
|
right_click_controller_->set_button(GDK_BUTTON_SECONDARY);
|
||||||
|
right_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_right_click_pressed));
|
||||||
|
add_controller(right_click_controller_);
|
||||||
|
|
||||||
|
// Motion (Hover)
|
||||||
|
motion_controller_ = Gtk::EventControllerMotion::create();
|
||||||
|
motion_controller_->signal_enter().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
|
||||||
|
motion_controller_->signal_motion().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
|
||||||
|
motion_controller_->signal_leave().connect(sigc::mem_fun(*this, &BoardWidget::on_leave));
|
||||||
|
add_controller(motion_controller_);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoardWidget::~BoardWidget() {
|
||||||
|
if (tick_id_ > 0) remove_tick_callback(tick_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::set_minefield(std::shared_ptr<Minefield> field) {
|
||||||
|
field_ = field;
|
||||||
|
particles_.clear();
|
||||||
|
if (tick_id_ > 0) {
|
||||||
|
remove_tick_callback(tick_id_);
|
||||||
|
tick_id_ = 0;
|
||||||
|
}
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::start_confetti() {
|
||||||
|
particles_.clear();
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
std::uniform_real_distribution<> pos_dist(0.0, 1.0); // Relative to window
|
||||||
|
std::uniform_real_distribution<> vel_dist(-2.0, 2.0);
|
||||||
|
std::uniform_real_distribution<> color_dist(0.0, 1.0);
|
||||||
|
std::uniform_real_distribution<> size_dist(3.0, 8.0);
|
||||||
|
|
||||||
|
int w = get_width();
|
||||||
|
int h = get_height();
|
||||||
|
|
||||||
|
for (int i = 0; i < 200; ++i) {
|
||||||
|
particles_.push_back({
|
||||||
|
pos_dist(gen) * w,
|
||||||
|
pos_dist(gen) * h * 0.5, // Start from top half
|
||||||
|
vel_dist(gen),
|
||||||
|
vel_dist(gen) + 2.0, // Fall down
|
||||||
|
1.0, // Life
|
||||||
|
color_dist(gen), color_dist(gen), color_dist(gen),
|
||||||
|
size_dist(gen)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tick_id_ == 0) {
|
||||||
|
tick_id_ = add_tick_callback(
|
||||||
|
sigc::mem_fun(*this, &BoardWidget::on_animation_tick)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BoardWidget::on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock) {
|
||||||
|
return process_animation(); // Overload
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BoardWidget::process_animation() {
|
||||||
|
if (particles_.empty()) return false;
|
||||||
|
|
||||||
|
bool alive = false;
|
||||||
|
for (auto& p : particles_) {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
p.vy += 0.1; // Gravity
|
||||||
|
p.life -= 0.01;
|
||||||
|
|
||||||
|
if (p.life > 0) alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_draw();
|
||||||
|
return alive;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> BoardWidget::get_cell_at(double x, double y) {
|
||||||
|
if (!field_) return {-1, -1};
|
||||||
|
|
||||||
|
// Use stored metrics from last draw
|
||||||
|
if (x < offset_x_ || x >= offset_x_ + field_->cols() * cell_size_ ||
|
||||||
|
y < offset_y_ || y >= offset_y_ + field_->rows() * cell_size_) {
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
int cx = static_cast<int>((x - offset_x_) / cell_size_);
|
||||||
|
int cy = static_cast<int>((y - offset_y_) / cell_size_);
|
||||||
|
return {cx, cy};
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_click_pressed(int n_press, double x, double y) {
|
||||||
|
if (!field_) return;
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx == -1) return;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (field_->get_cell(cx, cy).is_revealed) {
|
||||||
|
changed = field_->chord_cell(cx, cy);
|
||||||
|
} else {
|
||||||
|
changed = field_->open_cell(cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
signal_state_changed.emit();
|
||||||
|
if (field_->state() == GameState::Won) {
|
||||||
|
start_confetti();
|
||||||
|
}
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_right_click_pressed(int n_press, double x, double y) {
|
||||||
|
if (!field_) return;
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx == -1) return;
|
||||||
|
|
||||||
|
if (field_->toggle_flag(cx, cy)) {
|
||||||
|
signal_state_changed.emit();
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_motion(double x, double y) {
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx != hover_x_ || cy != hover_y_) {
|
||||||
|
hover_x_ = cx;
|
||||||
|
hover_y_ = cy;
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_leave() {
|
||||||
|
hover_x_ = -1;
|
||||||
|
hover_y_ = -1;
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
||||||
|
// Background
|
||||||
|
cr->set_source_rgb(0.95, 0.95, 0.95);
|
||||||
|
cr->paint();
|
||||||
|
|
||||||
|
if (field_) {
|
||||||
|
// Calculate scaling
|
||||||
|
int cols = field_->cols();
|
||||||
|
int rows = field_->rows();
|
||||||
|
|
||||||
|
double pad = 20.0;
|
||||||
|
double avail_w = width - 2 * pad;
|
||||||
|
double avail_h = height - 2 * pad;
|
||||||
|
|
||||||
|
cell_size_ = std::min(avail_w / cols, avail_h / rows);
|
||||||
|
cell_size_ = std::min(cell_size_, 48.0);
|
||||||
|
cell_size_ = std::max(cell_size_, 16.0);
|
||||||
|
|
||||||
|
double board_w_px = cols * cell_size_;
|
||||||
|
double board_h_px = rows * cell_size_;
|
||||||
|
offset_x_ = (width - board_w_px) / 2.0;
|
||||||
|
offset_y_ = (height - board_h_px) / 2.0;
|
||||||
|
|
||||||
|
cr->save();
|
||||||
|
cr->translate(offset_x_, offset_y_);
|
||||||
|
|
||||||
|
// Shadow for the board
|
||||||
|
cr->set_source_rgba(0.0, 0.0, 0.0, 0.1);
|
||||||
|
cr->rectangle(5, 5, board_w_px, board_h_px);
|
||||||
|
cr->fill();
|
||||||
|
|
||||||
|
for (int y = 0; y < rows; ++y) {
|
||||||
|
for (int x = 0; x < cols; ++x) {
|
||||||
|
draw_cell(cr, x, y, cell_size_, field_->get_cell(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cr->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!particles_.empty()) {
|
||||||
|
draw_particles(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell) {
|
||||||
|
double px = x * size;
|
||||||
|
double py = y * size;
|
||||||
|
|
||||||
|
// Base shape
|
||||||
|
cr->rectangle(px, py, size, size);
|
||||||
|
|
||||||
|
bool is_hover = (x == hover_x_ && y == hover_y_ && field_->state() == GameState::Playing);
|
||||||
|
bool revealed = cell.is_revealed;
|
||||||
|
|
||||||
|
if (revealed) {
|
||||||
|
if (cell.is_exploded) {
|
||||||
|
cr->set_source_rgb(0.9, 0.3, 0.3); // Red background
|
||||||
|
} else {
|
||||||
|
cr->set_source_rgb(0.92, 0.92, 0.92); // Revealed background (lighter gray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (is_hover) {
|
||||||
|
cr->set_source_rgb(0.75, 0.85, 1.0); // Hover light blue
|
||||||
|
} else {
|
||||||
|
// Gradient for unrevealed cells
|
||||||
|
auto pat = Cairo::LinearGradient::create(px, py, px + size, py + size);
|
||||||
|
pat->add_color_stop_rgba(0, 0.8, 0.8, 0.8, 1);
|
||||||
|
pat->add_color_stop_rgba(1, 0.7, 0.7, 0.7, 1);
|
||||||
|
cr->set_source(pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!revealed && !is_hover) cr->fill();
|
||||||
|
else cr->fill_preserve();
|
||||||
|
|
||||||
|
if (revealed || is_hover) {
|
||||||
|
// Border
|
||||||
|
cr->set_source_rgb(0.6, 0.6, 0.6);
|
||||||
|
cr->set_line_width(1.0);
|
||||||
|
cr->stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
if (cell.is_flagged) {
|
||||||
|
draw_flag(cr, px, py, size);
|
||||||
|
} else if (revealed) {
|
||||||
|
if (cell.is_bomb) {
|
||||||
|
draw_bomb(cr, px, py, size, cell.is_exploded);
|
||||||
|
} else if (cell.nearby_bombs > 0) {
|
||||||
|
draw_digit(cr, cell.nearby_bombs, px, py, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size) {
|
||||||
|
double cx = x + size / 2.0;
|
||||||
|
double cy = y + size / 2.0;
|
||||||
|
|
||||||
|
// Pole
|
||||||
|
cr->set_source_rgb(0.2, 0.2, 0.2);
|
||||||
|
cr->set_line_width(2.0);
|
||||||
|
cr->move_to(cx - size * 0.1, cy + size * 0.3);
|
||||||
|
cr->line_to(cx - size * 0.1, cy - size * 0.3);
|
||||||
|
cr->stroke();
|
||||||
|
|
||||||
|
// Flag
|
||||||
|
cr->set_source_rgb(0.9, 0.2, 0.2);
|
||||||
|
cr->move_to(cx - size * 0.1, cy - size * 0.3);
|
||||||
|
cr->line_to(cx + size * 0.25, cy - size * 0.15);
|
||||||
|
cr->line_to(cx - size * 0.1, cy);
|
||||||
|
cr->close_path();
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded) {
|
||||||
|
double cx = x + size / 2.0;
|
||||||
|
double cy = y + size / 2.0;
|
||||||
|
double r = size * 0.25;
|
||||||
|
|
||||||
|
// Bomb body
|
||||||
|
auto pat = Cairo::RadialGradient::create(cx - r*0.3, cy - r*0.3, r*0.1, cx, cy, r);
|
||||||
|
pat->add_color_stop_rgba(0, 0.4, 0.4, 0.4, 1);
|
||||||
|
pat->add_color_stop_rgba(1, 0.1, 0.1, 0.1, 1);
|
||||||
|
cr->set_source(pat);
|
||||||
|
cr->arc(cx, cy, r, 0, 2 * M_PI);
|
||||||
|
cr->fill();
|
||||||
|
|
||||||
|
// Spikes/Fuse
|
||||||
|
cr->set_source_rgb(0.1, 0.1, 0.1);
|
||||||
|
cr->set_line_width(2.0);
|
||||||
|
cr->move_to(cx, cy - r);
|
||||||
|
cr->line_to(cx, cy - r - size * 0.1);
|
||||||
|
cr->stroke();
|
||||||
|
|
||||||
|
// Spark if exploded?
|
||||||
|
if (exploded) {
|
||||||
|
cr->set_source_rgb(1.0, 0.5, 0.0);
|
||||||
|
cr->arc(cx, cy - r - size * 0.1, size * 0.05, 0, 2 * M_PI);
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size) {
|
||||||
|
switch (number) {
|
||||||
|
case 1: cr->set_source_rgb(0.2, 0.4, 1.0); break; // Blue
|
||||||
|
case 2: cr->set_source_rgb(0.2, 0.6, 0.2); break; // Green
|
||||||
|
case 3: cr->set_source_rgb(0.9, 0.2, 0.2); break; // Red
|
||||||
|
case 4: cr->set_source_rgb(0.1, 0.1, 0.6); break; // Dark Blue
|
||||||
|
case 5: cr->set_source_rgb(0.6, 0.1, 0.1); break; // Maroon
|
||||||
|
case 6: cr->set_source_rgb(0.1, 0.6, 0.6); break; // Cyan
|
||||||
|
case 7: cr->set_source_rgb(0.1, 0.1, 0.1); break; // Black
|
||||||
|
case 8: cr->set_source_rgb(0.5, 0.5, 0.5); break; // Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
cr->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::BOLD);
|
||||||
|
cr->set_font_size(size * 0.7); // Bigger font
|
||||||
|
|
||||||
|
Cairo::TextExtents extents;
|
||||||
|
std::string text = std::to_string(number);
|
||||||
|
cr->get_text_extents(text, extents);
|
||||||
|
|
||||||
|
cr->move_to(x + (size - extents.width) / 2 - extents.x_bearing,
|
||||||
|
y + (size - extents.height) / 2 - extents.y_bearing);
|
||||||
|
cr->show_text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_particles(const Cairo::RefPtr<Cairo::Context>& cr) {
|
||||||
|
for (const auto& p : particles_) {
|
||||||
|
if (p.life <= 0) continue;
|
||||||
|
cr->set_source_rgba(p.r, p.g, p.b, p.life);
|
||||||
|
cr->rectangle(p.x, p.y, p.size, p.size);
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/board_widget.hpp
Normal file
69
src/board_widget.hpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtkmm/drawingarea.h>
|
||||||
|
#include <gtkmm/gestureclick.h>
|
||||||
|
#include <gtkmm/eventcontrollermotion.h>
|
||||||
|
#include <gdkmm/texture.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <random>
|
||||||
|
#include "minefield.hpp"
|
||||||
|
|
||||||
|
struct Particle {
|
||||||
|
double x, y;
|
||||||
|
double vx, vy;
|
||||||
|
double life;
|
||||||
|
double r, g, b;
|
||||||
|
double size;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BoardWidget : public Gtk::DrawingArea {
|
||||||
|
public:
|
||||||
|
BoardWidget();
|
||||||
|
virtual ~BoardWidget();
|
||||||
|
|
||||||
|
void set_minefield(std::shared_ptr<Minefield> field);
|
||||||
|
|
||||||
|
// Signal when state changes (for UI updates)
|
||||||
|
sigc::signal<void()> signal_state_changed;
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
void start_confetti();
|
||||||
|
bool on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock);
|
||||||
|
bool process_animation(); // Internal helper
|
||||||
|
|
||||||
|
private:
|
||||||
|
void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
|
||||||
|
|
||||||
|
// Input handlers
|
||||||
|
void on_click_pressed(int n_press, double x, double y);
|
||||||
|
void on_right_click_pressed(int n_press, double x, double y);
|
||||||
|
void on_motion(double x, double y);
|
||||||
|
void on_leave();
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
std::pair<int, int> get_cell_at(double x, double y);
|
||||||
|
void draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell);
|
||||||
|
void draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size);
|
||||||
|
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size);
|
||||||
|
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded);
|
||||||
|
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
|
||||||
|
|
||||||
|
std::shared_ptr<Minefield> field_;
|
||||||
|
|
||||||
|
// State
|
||||||
|
double cell_size_ = 32.0;
|
||||||
|
double offset_x_ = 0;
|
||||||
|
double offset_y_ = 0;
|
||||||
|
int hover_x_ = -1;
|
||||||
|
int hover_y_ = -1;
|
||||||
|
|
||||||
|
// Particles
|
||||||
|
std::vector<Particle> particles_;
|
||||||
|
guint tick_id_ = 0;
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
Glib::RefPtr<Gtk::GestureClick> left_click_controller_;
|
||||||
|
Glib::RefPtr<Gtk::GestureClick> right_click_controller_;
|
||||||
|
Glib::RefPtr<Gtk::EventControllerMotion> motion_controller_;
|
||||||
|
};
|
||||||
8
src/main.cpp
Normal file
8
src/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "window.hpp"
|
||||||
|
#include <gtkmm/application.h>
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
auto app = Gtk::Application::create("org.gtkmm.minesweeper");
|
||||||
|
|
||||||
|
return app->make_window_and_run<MainWindow>(argc, argv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user