monorepo/otg/gtk/src/components/goban.rs

306 lines
10 KiB
Rust

/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of On the Grid.
On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
// I have an old Board class which I'm going to update. I'll just copy over the rendering code, but
// at the same time I am going to work pretty heavily on the API.
//
// For a game review, the board needs to interact very well with a game record. So I have to keep
// in mind all of that as I work on the API.
//
// Also, this is going to be a cross-platform application. Today it is Gnome + Rust, but as I
// progress I will also need a Progressive Web App so that I can run this on my tablet. Especially
// useful if I'm out socializing and happen to be talking to somebody who would enjoy a relaxing
// game. Anyway, that is going to impact some of my API decisions.
//
// First, though, I need to rename my game record.
//
// Now, let's get the existing code compiling again.
//
// Okay, that wasn't so bad. I'm a little confused that I don't have a code action for renaming a
// symbol, but I'll fix that some other time. Anyway, now let's focus on the goban.
// Now, we know what kind of object we have for the current board representation. Let's make use of
// that.
use crate::{perftrace, Resource, ResourceManager};
use glib::Object;
use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*};
use otg_core::{Color, Coordinate};
use std::{cell::RefCell, rc::Rc};
const WIDTH: i32 = 800;
const HEIGHT: i32 = 800;
const MARGIN: i32 = 20;
// Internal representation of the Goban drawing area.
#[derive(Default)]
pub struct GobanPrivate {
board_state: Rc<RefCell<otg_core::Goban>>,
resource_manager: Rc<RefCell<Option<ResourceManager>>>,
}
impl GobanPrivate {}
#[glib::object_subclass]
impl ObjectSubclass for GobanPrivate {
const NAME: &'static str = "Goban";
type Type = Goban;
type ParentType = gtk::DrawingArea;
}
impl ObjectImpl for GobanPrivate {}
impl WidgetImpl for GobanPrivate {}
impl DrawingAreaImpl for GobanPrivate {}
// This Goban, being in the `components` crate, is merely the rendering of a board. This is not
// the primary representation of the board.
//
// In a game of Go, there are certain rules about what are valid moves and what are not.
// Internally, I want to keep track of those, and doing so requires a few things.
//
// - We can never repeat a game state (though I think maybe that is allowed in a few rulesets, but
// I'm coding to the AGA ruleset)
// - We can never play a suicidal move
//
// Finally, updating the board state is non-GUI logic. So, sorry, might be dropping away from GUI
// code for a while to work on the backend representation, some of which already exists.
glib::wrapper! {
pub struct Goban(ObjectSubclass<GobanPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl Goban {
pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build();
*s.imp().board_state.borrow_mut() = board_state;
*s.imp().resource_manager.borrow_mut() = Some(resources);
s.set_width_request(WIDTH);
s.set_height_request(HEIGHT);
s.set_draw_func({
let s = s.clone();
move |_, ctx, width, height| {
perftrace("render drawing area", || s.redraw(ctx, width, height));
}
});
s
}
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
println!("{} x {}", width, height);
/*
let background = load_pixbuf(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
false,
WIDTH + 40,
HEIGHT + 40,
);
*/
let background = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg"));
let black_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png"));
let white_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png"));
match background {
Some(Resource::Image(ref background)) => {
ctx.set_source_pixbuf(background, 0., 0.);
ctx.paint().expect("paint should never fail");
}
None => ctx.set_source_rgb(0.7, 0.7, 0.7),
}
let board = self.imp().board_state.borrow();
ctx.set_source_rgb(0.1, 0.1, 0.1);
ctx.set_line_width(2.);
let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);
let pen = Pen::new(
MARGIN as f64,
MARGIN as f64,
hspace_between,
vspace_between,
black_texture,
white_texture,
);
(0..board.size.width).for_each(|col| {
ctx.move_to(
(MARGIN as f64) + (col as f64) * hspace_between,
MARGIN as f64,
);
ctx.line_to(
(MARGIN as f64) + (col as f64) * hspace_between,
(height as f64) - (MARGIN as f64),
);
let _ = ctx.stroke();
});
(0..board.size.height).for_each(|row| {
ctx.move_to(
MARGIN as f64,
(MARGIN as f64) + (row as f64) * vspace_between,
);
ctx.line_to(
(width - MARGIN) as f64,
(MARGIN as f64) + (row as f64) * vspace_between,
);
let _ = ctx.stroke();
});
// This doesn't work except on 19x19 boads. This code needs to be adjusted to at least
// handle 13x13 and 9x9. Non-standard boards are On Their Own (TM).
vec![3, 9, 15].into_iter().for_each(|col| {
vec![3, 9, 15].into_iter().for_each(|row| {
pen.star_point(ctx, col, row);
});
});
(0..board.size.height).for_each(|row| {
(0..board.size.width).for_each(|column| {
match board.stone(&Coordinate { row, column }) {
None => {}
Some(Color::White) => pen.stone(ctx, row, column, Color::White, None),
Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None),
}
})
})
}
}
struct Pen {
x_offset: f64,
y_offset: f64,
hspace_between: f64,
vspace_between: f64,
black_stone: Option<Pixbuf>,
white_stone: Option<Pixbuf>,
}
impl Pen {
fn new(
x_offset: f64,
y_offset: f64,
hspace_between: f64,
vspace_between: f64,
black_stone: Option<Resource>,
white_stone: Option<Resource>,
) -> Self {
let black_stone = match black_stone {
Some(Resource::Image(img)) => Some(img),
_ => None,
};
let white_stone = match white_stone {
Some(Resource::Image(img)) => Some(img),
_ => None,
};
Pen {
x_offset,
y_offset,
hspace_between,
vspace_between,
black_stone,
white_stone,
}
}
fn star_point(&self, context: &cairo::Context, row: u8, col: u8) {
context.arc(
self.x_offset + (col as f64) * self.hspace_between,
self.y_offset + (row as f64) * self.vspace_between,
5.,
0.,
2. * std::f64::consts::PI,
);
let _ = context.fill();
}
fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) {
let (x_loc, y_loc) = self.stone_location(row, col);
match color {
Color::White => match self.white_stone {
Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
},
Color::Black => match self.black_stone {
Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
},
}
ctx.paint().expect("paint should never fail");
/*
match color {
Color::White => ctx.set_source_rgb(0.9, 0.9, 0.9),
Color::Black => ctx.set_source_rgb(0.0, 0.0, 0.0),
};
self.draw_stone(ctx, row, col);
*/
/*
if let Some(liberties) = liberties {
let stone_location = self.stone_location(row, col);
context.set_source_rgb(1., 0., 1.);
context.set_font_size(32.);
context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
let _ = context.show_text(&format!("{}", liberties));
}
*/
}
#[allow(dead_code)]
fn ghost_stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color) {
match color {
Color::White => ctx.set_source_rgba(0.9, 0.9, 0.9, 0.5),
Color::Black => ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5),
};
self.draw_stone(ctx, row, col);
}
fn draw_stone(&self, ctx: &cairo::Context, row: u8, col: u8) {
let radius = self.hspace_between / 2. - 2.;
let (x_loc, y_loc) = self.stone_location(row, col);
ctx.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
let _ = ctx.fill();
}
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
let radius = self.hspace_between / 2. - 2.;
(
self.x_offset + (col as f64) * self.hspace_between - radius,
self.y_offset + (row as f64) * self.vspace_between - radius,
)
}
}