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