Set up the game review page along with #229

Merged
savanni merged 24 commits from otg/game-review into main 2024-03-31 23:37:51 +00:00
31 changed files with 2639 additions and 268 deletions

2
Cargo.lock generated
View File

@ -2709,6 +2709,7 @@ dependencies = [
"serde_json",
"sgf",
"thiserror",
"uuid 0.8.2",
]
[[package]]
@ -2728,6 +2729,7 @@ dependencies = [
"pango",
"sgf",
"tokio",
"uuid 0.8.2",
]
[[package]]

View File

@ -15,6 +15,7 @@ grid = { version = "0.9" }
serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] }
thiserror = { version = "1" }
uuid = { version = "0.8", features = ["v4", "serde"] }
[dev-dependencies]
cool_asserts = { version = "2" }

View File

@ -17,19 +17,11 @@ You should have received a copy of the GNU General Public License along with On
use crate::{
database::Database,
library, settings,
types::{AppState, Config, ConfigOption, GameState, LibraryPath, Player, Rank},
};
use async_std::{
channel::{Receiver, Sender},
stream,
task::spawn,
types::{Config, LibraryPath},
};
use async_std::channel::{Receiver, Sender};
use serde::{Deserialize, Serialize};
use std::{
future::Future,
path::PathBuf,
sync::{Arc, RwLock, RwLockReadGuard},
};
use std::sync::{Arc, RwLock, RwLockReadGuard};
pub trait Observable<T> {
fn subscribe(&self) -> Receiver<T>;
@ -168,14 +160,14 @@ impl Core {
*self.library.write().unwrap() = Some(db);
}
pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option<Database>> {
pub fn library(&self) -> RwLockReadGuard<'_, Option<Database>> {
self.library.read().unwrap()
}
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
match request {
CoreRequest::Library(request) => library::handle(&self, request).await.into(),
CoreRequest::Settings(request) => settings::handle(&self, request).await.into(),
CoreRequest::Library(request) => library::handle(self, request).await.into(),
CoreRequest::Settings(request) => settings::handle(self, request).await.into(),
}
}

View File

@ -1,6 +1,6 @@
use std::{io::Read, path::PathBuf};
use sgf::{parse_sgf, Game};
use sgf::{parse_sgf, GameRecord};
use thiserror::Error;
#[derive(Error, Debug)]
@ -21,12 +21,12 @@ impl From<std::io::Error> for Error {
#[derive(Debug)]
pub struct Database {
games: Vec<Game>,
games: Vec<GameRecord>,
}
impl Database {
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
let mut games: Vec<Game> = Vec::new();
let mut games: Vec<GameRecord> = Vec::new();
let extension = PathBuf::from("sgf").into_os_string();
@ -42,13 +42,10 @@ impl Database {
.unwrap();
match parse_sgf(&buffer) {
Ok(sgfs) => {
for sgf in sgfs {
if let Ok(sgf) = sgf {
games.push(sgf);
}
}
let mut sgfs = sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
games.append(&mut sgfs);
}
Err(err) => println!("Error parsing {:?}", entry.path()),
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
}
}
}
@ -59,7 +56,7 @@ impl Database {
Ok(Database { games })
}
pub fn all_games(&self) -> impl Iterator<Item = &Game> {
pub fn all_games(&self) -> impl Iterator<Item = &GameRecord> {
self.games.iter()
}
}
@ -84,11 +81,11 @@ mod test {
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
assert_eq!(db.all_games().count(), 5);
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
assert_matches!(db.all_games().find(|g| g.black_player.name == Some("Steve".to_owned())),
Some(game) => {
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
assert_eq!(game.black_player.name, Some("Steve".to_owned()));
assert_eq!(game.white_player.name, Some("Savanni".to_owned()));
assert_eq!(game.dates, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
// assert_eq!(game.info.komi, Some(6.5));
}
);

View File

@ -1,9 +1,35 @@
/*
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/>.
*/
// TBQH, I don't recall what state this object is in, but I do know that I might have some troubles
// integrating it with a game record. Some of the time here is going to be me reading (and
// documenting) my code from almost a year ago.
//
use crate::{BoardError, Color, Size};
use sgf::{GameNode, MoveNode};
use std::collections::HashSet;
#[derive(Clone, Debug, Default)]
pub struct Goban {
/// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game.
/// These are fun for novelty, but don't lend much to understanding the game.
pub size: Size,
/// I found that it was easiest to track groups of stones than to track individual stones on the
/// board. So, I just keep track of all of the groups.
pub groups: Vec<Group>,
}
@ -62,12 +88,20 @@ impl Goban {
}
}
/// Generate a board state from an iterator of coordinates and the color of any stone present on
/// the board. As we walk through the iterator, we play each stone as though it were being
/// played in a game.
///
/// This would not work at all if we wanted to set up an impossible board state, given that
/// groups of stones get automatically removed once surrounded.
pub fn from_coordinates(
mut coordinates: impl Iterator<Item = (Coordinate, Color)>,
coordinates: impl IntoIterator<Item = (Coordinate, Color)>,
) -> Result<Self, BoardError> {
coordinates.try_fold(Self::new(), |board, (coordinate, color)| {
board.place_stone(coordinate, color)
})
coordinates
.into_iter()
.try_fold(Self::new(), |board, (coordinate, color)| {
board.place_stone(coordinate, color)
})
}
}
@ -78,37 +112,77 @@ pub struct Coordinate {
}
impl Goban {
/// place_stone is the most fundamental function of this object. This is as though a player put
/// a stone on the board and evaluated the consequences.
///
/// This function does not enforce turn order.
///
/// # Examples
///
/// ```
/// use otg_core::{Color, Size, Coordinate, Goban};
/// use cool_asserts::assert_matches;
///
/// let goban = Goban::new();
/// assert_eq!(goban.size, Size{ width: 19, height: 19 });
/// let move_result = goban.place_stone(Coordinate{ column: 4, row: 4 }, Color::Black);
/// assert_matches!(move_result, Goban);
/// ```
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
// Bail out immediately if there is already a stone at this location.
if self.stone(&coordinate).is_some() {
return Err(BoardError::InvalidPosition);
}
// Find all friendly groups adjacent to this stone. First, calculate the adjacent
// coordinates. Then see if there is any group which contains that coordinate. If not, this
// stone forms a new group of its own.
//
// A little subtle here is that this stone will be added to *every* adjoining friendly
// group. This normally means only that a group gets bigger, but it could also cause two
// groups to share a stone, which means they're now a single group.
let mut friendly_group = self
.adjacencies(&coordinate)
.into_iter()
.filter(|c| self.stone(c) == Some(color))
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
// In fact, this last step actually connects the coordinates of those friendly groups
// into a single large group.
.fold(HashSet::new(), |acc, set| {
acc.union(&set).cloned().collect()
});
// This is a little misnamed. This is a HashSet, not a full Group.
friendly_group.insert(coordinate);
// Remove all groups which contain the stones overlapping with this friendly group.
self.groups
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
// Generate a new friendly group given the coordinates.
let friendly_group = Group {
color,
coordinates: friendly_group,
};
// Now add the group back to the board.
self.groups.push(friendly_group.clone());
// Now, find all groups adjacent to this one. Those are the only groups that this move is
// going to impact. Calculate their liberties.
let adjacent_groups = self.adjacent_groups(&friendly_group);
for group in adjacent_groups {
// Any group that has been reduced to 0 liberties should now be removed from the board.
//
// TODO: capture rules: we're not counting captured stones yet. Okay with some scoring
// methods, but not all.
if self.liberties(&group) == 0 {
self.remove_group(&group);
}
}
// Now, recalculate the liberties of this friendly group. If this group has been reduced to
// zero liberties, after all captures have been accounted for, the move is an illegal
// self-capture. Drop all of the work we've done and return an error.
if self.liberties(&friendly_group) == 0 {
return Err(BoardError::SelfCapture);
}
@ -116,6 +190,53 @@ impl Goban {
Ok(self)
}
/// Apply a list of moves to the board and return the final board. The moves will be played as
/// though they are live moves played normally, but this function is for generating a board
/// state from a game record. All of the moves will be played in the order given. This does not
/// allow for the branching which is natural in a game review.
///
/// # Examples
///
/// ```
/// use otg_core::{Color, Size, Coordinate, Goban};
/// use cool_asserts::assert_matches;
/// use sgf::{GameNode, MoveNode, Move};
///
/// let goban = Goban::new();
/// let moves = vec![
/// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dd".to_owned()))),
/// GameNode::MoveNode(MoveNode::new(sgf::Color::White, Move::Move("pp".to_owned()))),
/// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()))),
/// ];
/// let moves_: Vec<&GameNode> = moves.iter().collect();
/// let goban = goban.apply_moves(moves_).expect("the test to have valid moves");
///
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White));
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 15 }), Some(Color::Black));
/// ```
pub fn apply_moves<'a>(
self,
moves: impl IntoIterator<Item = &'a GameNode>,
) -> Result<Goban, BoardError> {
let mut s = self;
for m in moves.into_iter() {
match m {
GameNode::MoveNode(node) => s = s.apply_move_node(node)?,
GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"),
};
}
Ok(s)
}
fn apply_move_node(self, m: &MoveNode) -> Result<Goban, BoardError> {
if let Some((row, column)) = m.mv.coordinate() {
self.place_stone(Coordinate { row, column }, Color::from(&m.color))
} else {
Ok(self)
}
}
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
self.groups
.iter()
@ -123,17 +244,17 @@ impl Goban {
.map(|g| g.color)
}
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
self.groups
.iter()
.find(|g| g.coordinates.contains(coordinate))
}
pub fn remove_group(&mut self, group: &Group) {
fn remove_group(&mut self, group: &Group) {
self.groups.retain(|g| g != group);
}
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
let adjacent_spaces = self.group_halo(group).into_iter();
let mut grps: Vec<Group> = Vec::new();
@ -153,7 +274,7 @@ impl Goban {
grps
}
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
group
.coordinates
.iter()
@ -161,14 +282,14 @@ impl Goban {
.collect::<HashSet<Coordinate>>()
}
pub fn liberties(&self, group: &Group) -> usize {
fn liberties(&self, group: &Group) -> usize {
self.group_halo(group)
.into_iter()
.filter(|c| self.stone(c).is_none())
.count()
}
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
let mut v = Vec::new();
if coordinate.column > 0 {
v.push(Coordinate {
@ -193,7 +314,7 @@ impl Goban {
v.into_iter().filter(|c| self.within_board(c)).collect()
}
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
fn within_board(&self, coordinate: &Coordinate) -> bool {
coordinate.column < self.size.width && coordinate.row < self.size.height
}
}

View File

@ -19,14 +19,15 @@ extern crate config_derive;
mod api;
pub use api::{Core, CoreNotification, CoreRequest, CoreResponse, Observable};
mod board;
pub use board::*;
mod goban;
pub use goban::*;
mod database;
pub mod library;
mod types;
pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size};
pub mod settings;
mod types;
pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree};

View File

@ -14,9 +14,9 @@ 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/>.
*/
use crate::{Core, Config};
use crate::{Core};
use serde::{Deserialize, Serialize};
use sgf::Game;
use sgf::GameRecord;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LibraryRequest {
@ -25,14 +25,14 @@ pub enum LibraryRequest {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LibraryResponse {
Games(Vec<Game>)
Games(Vec<GameRecord>)
}
async fn handle_list_games(model: &Core) -> LibraryResponse {
let library = model.library();
match *library {
Some(ref library) => {
let info = library.all_games().map(|g| g.clone()).collect::<Vec<Game>>();
let info = library.all_games().cloned().collect::<Vec<GameRecord>>();
LibraryResponse::Games(info)
}
None => LibraryResponse::Games(vec![]),

View File

@ -14,7 +14,7 @@ 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/>.
*/
use crate::{types::LibraryPath, Core, Config};
use crate::{Core, Config};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -1,12 +1,11 @@
use crate::{
board::{Coordinate, Goban},
database::Database,
};
use crate::goban::{Coordinate, Goban};
use config::define_config;
use config_derive::ConfigOption;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
use sgf::GameNode;
use std::{cell::RefCell, collections::VecDeque, fmt, path::PathBuf, time::Duration};
use thiserror::Error;
use uuid::Uuid;
define_config! {
LibraryPath(LibraryPath),
@ -55,6 +54,24 @@ pub enum Color {
White,
}
impl From<&sgf::Color> for Color {
fn from(c: &sgf::Color) -> Self {
match c {
sgf::Color::Black => Self::Black,
sgf::Color::White => Self::White,
}
}
}
impl fmt::Display for Color {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Color::Black => write!(formatter, "Black"),
Color::White => write!(formatter, "White"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Size {
pub width: u8,
@ -70,6 +87,32 @@ impl Default for Size {
}
}
/// AppState stores all of the important state to a full running version of the application.
/// However, this version of AppState is in pretty sorry shape.
///
/// What are the states of the app?
///
/// - in review
/// - in a game
/// - connections to the internet
/// - connections to local applications, such as Leela Zero
/// - the current configuration
/// - the games database
/// - If in a game, the current state of the game. Delegated to GameState.
/// - If in review, the current state of the review.
///
/// Some of these states are concurrent. It's quite possible to have online connections running and
/// to be reviewing a game while, for instance, waiting for an opponent on OGS to make a move.
///
/// I get to ignore a lot of these things for now. Not playing online. Not playing at all,
/// actually. We'll come back to that.
///
/// Plus, it gets more fuzzy, because some of the application state is really UI state. For
/// instance, the state of a game review is purely UI.
///
/// So... AppState probably isn't great for now, but maybe it will become so later. I think I'm
/// going to ignore it until I need it.
/*
#[derive(Debug)]
pub struct AppState {
pub game: Option<GameState>,
@ -95,6 +138,7 @@ impl AppState {
}
*/
}
*/
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Rank {
@ -164,6 +208,9 @@ impl Default for GameState {
}
impl GameState {
// Legacy code. I recall that this is no longer used (but will be used again) because I
// commented out so much code when I was overhauling the architecture of this app.
#[allow(dead_code)]
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
let board = self.board.clone();
let new_board = board.place_stone(coordinate, self.current_player)?;
@ -182,9 +229,193 @@ impl GameState {
}
}
// To properly generate a tree, I need to know how deep to go. Then I can backtrace. Each node
// needs to have a depth. Given a tree, the depth of the node is just the distance from the root.
// This seems obvious, but I had to write it to discover how important that fact was.
//
// So, what is the maximum depth of the tree? Follow all paths and see how far I get in every case.
// I could do this by just generating an intermediate tree and numbering each level.
pub struct Tree<T> {
nodes: Vec<Node<T>>,
}
#[derive(Debug)]
pub struct Node<T> {
pub id: usize,
node: T,
parent: Option<usize>,
depth: usize,
width: RefCell<Option<usize>>,
children: Vec<usize>,
}
impl<T> Tree<T> {
fn new(root: T) -> Self {
Tree {
nodes: vec![Node {
id: 0,
node: root,
parent: None,
depth: 0,
width: RefCell::new(None),
children: vec![],
}],
}
}
pub fn node(&self, idx: usize) -> &T {
&self.nodes[idx].node
}
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
// to add the root node, but the constructor should handle that, anyway.
fn add_node(&mut self, parent_idx: usize, node: T) -> usize {
let next_idx = self.nodes.len();
let parent = &self.nodes[parent_idx];
self.nodes.push(Node {
id: next_idx,
node,
parent: Some(parent_idx),
depth: parent.depth + 1,
width: RefCell::new(None),
children: vec![],
});
let parent = &mut self.nodes[parent_idx];
parent.children.push(next_idx);
next_idx
}
pub fn max_depth(&self) -> usize {
self.nodes.iter().fold(
0,
|max, node| if node.depth > max { node.depth } else { max },
)
}
// Since I know the width of a node, now I want to figure out its placement in the larger
// scheme of things.
//
// One thought I have is that I could just develop a grid virtually and start placing nodes.
// Whenever I notice a collision, I can just move the node over. But I'd like to see if I can
// be a bit smarter than doing it as just a vec into which I place things, as though it's a
// game board. So, given a game node, I want to figure out it's position along the X axis.
//
// Just having the node is greatly insufficient. I can get better results if I'm calculating
// the position of its children.
//
// indent represents the indentation that should be applied to all children in this tree. It
// amounts to the position of the parent node.
//
// When drawing nodes, I don't know how to persist the level of indent.
pub fn position(&self, idx: usize) -> (usize, usize) {
let node = &self.nodes[idx];
match node.parent {
Some(parent_idx) => {
let (_parent_row, parent_column) = self.position(parent_idx);
let parent = &self.nodes[parent_idx];
let sibling_width = parent
.children
.iter()
.take_while(|n| **n != node.id)
.fold(0, |acc, n| acc + self.width(*n));
(node.depth, parent_column + sibling_width)
}
// Root nodes won't have a parent, so just put them in the first column
None => (0, 0),
}
}
// Given a node, do a postorder traversal to figure out the width of the node based on all of
// its children. This is equivalent to the widest of all of its children at all depths.
//
// There are some collapse rules that I could take into account here, but that I haven't
// figured out yet. If two nodes are side by side, and one of them has some wide children but
// the other has no children, then they are effectively the same width. The second node only
// needs to be moved out if it has children that would overlap the children of the first node.
//
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game
// review.
fn width(&self, id: usize) -> usize {
let node = &self.nodes[id];
if let Some(width) = *node.width.borrow() {
return width;
}
let width = node
.children
.iter()
.fold(0, |acc, child| acc + self.width(*child));
let width = if width == 0 { 1 } else { width };
*node.width.borrow_mut() = Some(width);
width
}
pub fn bfs_iter(&self) -> BFSIter<T> {
let mut queue = VecDeque::new();
queue.push_back(&self.nodes[0]);
BFSIter { tree: self, queue }
}
}
impl<'a> From<&'a GameNode> for Tree<Uuid> {
fn from(root: &'a GameNode) -> Self {
fn add_subtree(tree: &mut Tree<Uuid>, parent_idx: usize, node: &GameNode) {
let idx = tree.add_node(parent_idx, node.id());
let children = match node {
GameNode::MoveNode(node) => &node.children,
GameNode::SetupNode(node) => &node.children,
};
for child in children {
add_subtree(tree, idx, child);
}
}
let mut tree = Tree::new(root.id());
let children = match root {
GameNode::MoveNode(node) => &node.children,
GameNode::SetupNode(node) => &node.children,
};
for node in children {
add_subtree(&mut tree, 0, node);
}
tree
}
}
pub struct BFSIter<'a, T> {
tree: &'a Tree<T>,
queue: VecDeque<&'a Node<T>>,
}
impl<'a, T> Iterator for BFSIter<'a, T> {
type Item = &'a Node<T>;
fn next(&mut self) -> Option<Self::Item> {
let retval = self.queue.pop_front();
if let Some(retval) = retval {
retval
.children
.iter()
.for_each(|idx| self.queue.push_back(&self.tree.nodes[*idx]));
}
retval
}
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use sgf::{Move, MoveNode};
#[test]
fn current_player_changes_after_move() {
@ -238,4 +469,121 @@ mod test {
Err(BoardError::Ko)
);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_can_calculate_depth_from_game_tree() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
node_b.children.push(GameNode::MoveNode(node_c));
node_h.children.push(GameNode::MoveNode(node_i));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
let game_tree = GameNode::MoveNode(node_a);
let tree = Tree::from(&game_tree);
assert_eq!(tree.max_depth(), 3);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_calculates_horizontal_position_of_nodes() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
node_b.children.push(GameNode::MoveNode(node_c));
node_h.children.push(GameNode::MoveNode(node_i));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
let game_tree = GameNode::MoveNode(node_a);
let tree = Tree::from(&game_tree);
assert_eq!(tree.position(2), (2, 0));
assert_eq!(tree.position(1), (1, 0));
assert_eq!(tree.position(0), (0, 0));
assert_eq!(tree.position(4), (3, 1));
assert_eq!(tree.position(5), (3, 2));
assert_eq!(tree.position(6), (1, 3));
assert_eq!(tree.position(7), (1, 4));
}
#[test]
fn breadth_first_iter() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d.clone()));
node_c.children.push(GameNode::MoveNode(node_e.clone()));
node_c.children.push(GameNode::MoveNode(node_f.clone()));
node_b.children.push(GameNode::MoveNode(node_c.clone()));
node_h.children.push(GameNode::MoveNode(node_i.clone()));
node_a.children.push(GameNode::MoveNode(node_b.clone()));
node_a.children.push(GameNode::MoveNode(node_g.clone()));
node_a.children.push(GameNode::MoveNode(node_h.clone()));
let game_tree = GameNode::MoveNode(node_a.clone());
let tree = Tree::from(&game_tree);
let mut iter = tree.bfs_iter();
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_a.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_b.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_g.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_h.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_c.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_i.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_d.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_e.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_f.id));
}
}

View File

@ -21,6 +21,7 @@ otg-core = { path = "../core" }
pango = { version = "*" }
sgf = { path = "../../sgf" }
tokio = { version = "1.26", features = [ "full" ] }
uuid = { version = "0.8", features = ["v4", "serde"] }
[build-dependencies]
glib-build-tools = "0.17"

View File

@ -15,3 +15,15 @@
.preference-item > suffixes {
margin: 4px;
}
.player-card {
border-radius: 5px;
}
.player-card___black {
background-color: black;
}
.player-card___white {
background-color: white;
}

View File

@ -16,19 +16,22 @@ You should have received a copy of the GNU General Public License along with On
use crate::CoreApi;
use adw::prelude::*;
use async_std::task::{block_on, spawn};
use otg_core::{
settings::{SettingsRequest, SettingsResponse},
Config, CoreRequest, CoreResponse,
CoreRequest, CoreResponse,
};
use sgf::GameRecord;
use std::sync::{Arc, RwLock};
use crate::views::{HomeView, SettingsView};
use crate::views::{GameReview, HomeView, SettingsView};
/*
#[derive(Clone)]
enum AppView {
Home,
}
*/
// An application window should generally contain
// - an overlay widget
@ -37,7 +40,6 @@ enum AppView {
#[derive(Clone)]
pub struct AppWindow {
pub window: adw::ApplicationWindow,
header: adw::HeaderBar,
// content is a stack which contains the view models for the application. These are the main
// elements that users want to interact with: the home page, the game library, a review, a game
@ -46,11 +48,11 @@ pub struct AppWindow {
// we can maintain the state of previous views. Since the two of these work together, they are
// a candidate for extraction into a new widget or a new struct.
stack: adw::NavigationView,
content: Vec<AppView>,
// view_states: Vec<AppView>,
// Overlays are for transient content, such as about and settings, which can be accessed from
// anywhere but shouldn't be part of the main application flow.
panel_overlay: gtk::Overlay,
overlay: gtk::Overlay,
core: CoreApi,
// Not liking this, but I have to keep track of the settings view model separately from
@ -61,28 +63,45 @@ pub struct AppWindow {
impl AppWindow {
pub fn new(app: &adw::Application, core: CoreApi) -> Self {
let window = Self::setup_window(app);
let header = Self::setup_header();
let panel_overlay = Self::setup_panel_overlay();
let (stack, content) = Self::setup_content(core.clone());
let overlay = Self::setup_overlay();
let stack = adw::NavigationView::new();
// let view_states = vec![];
window.set_content(Some(&overlay));
overlay.set_child(Some(&stack));
let s = Self {
window,
stack,
// view_states,
overlay,
core,
settings_view_model: Default::default(),
};
let home = s.setup_home();
s.stack.push(&home);
s
}
pub fn open_game_review(&self, game_record: GameRecord) {
let header = adw::HeaderBar::new();
let game_review = GameReview::new(self.core.clone(), game_record);
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
layout.append(&header);
layout.append(&panel_overlay);
panel_overlay.set_child(Some(&stack));
layout.append(&game_review);
window.set_content(Some(&layout));
Self {
window,
header,
stack,
content,
panel_overlay,
core,
settings_view_model: Default::default(),
}
let page = adw::NavigationPage::builder()
.can_pop(true)
.title("Game Review")
.child(&layout)
.build();
self.stack.push(&page);
}
pub fn open_settings(&self) {
@ -123,7 +142,7 @@ impl AppWindow {
}
},
);
s.panel_overlay.add_overlay(&view_model);
s.overlay.add_overlay(&view_model);
*s.settings_view_model.write().unwrap() = Some(view_model);
}
}
@ -132,28 +151,23 @@ impl AppWindow {
pub fn close_overlay(&self) {
let mut view = self.settings_view_model.write().unwrap();
match *view {
Some(ref mut settings) => {
self.panel_overlay.remove_overlay(settings);
*view = None;
}
None => {}
if let Some(ref mut settings) = *view {
self.overlay.remove_overlay(settings);
*view = None;
}
}
fn setup_window(app: &adw::Application) -> adw::ApplicationWindow {
let window = adw::ApplicationWindow::builder()
adw::ApplicationWindow::builder()
.application(app)
.width_request(800)
.height_request(500)
.build();
window
.build()
}
fn setup_header() -> adw::HeaderBar {
let header = adw::HeaderBar::builder()
.title_widget(&gtk::Label::new(Some("Kifu")))
.title_widget(&gtk::Label::new(Some("On the Grid")))
.build();
let app_menu = gio::Menu::new();
@ -169,28 +183,27 @@ impl AppWindow {
header
}
fn setup_panel_overlay() -> gtk::Overlay {
fn setup_overlay() -> gtk::Overlay {
gtk::Overlay::new()
}
fn setup_content(core: CoreApi) -> (adw::NavigationView, Vec<AppView>) {
let stack = adw::NavigationView::new();
let content = Vec::new();
fn setup_home(&self) -> adw::NavigationPage {
let header = Self::setup_header();
let home = HomeView::new(self.core.clone(), {
let s = self.clone();
move |game| s.open_game_review(game)
});
let home = HomeView::new(core.clone());
let _ = stack.push(
&adw::NavigationPage::builder()
.can_pop(false)
.title("Kifu")
.child(&home)
.build(),
);
// content.push(AppView::Home(HomeViewModel::new(core)));
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
layout.append(&header);
layout.append(&home);
(stack, content)
adw::NavigationPage::builder()
.can_pop(false)
.title("Home")
.child(&layout)
.build()
}
// pub fn set_content(content: &impl IsA<gtk::Widget>) -> adw::ViewStack {
// self.content.set_child(Some(content));
// }
}

View File

@ -8,7 +8,6 @@ use gtk::{
};
use image::io::Reader as ImageReader;
use otg_core::{
ui::{BoardElement, IntersectionElement},
Color,
};
use std::{cell::RefCell, io::Cursor, rc::Rc};
@ -27,7 +26,7 @@ pub struct BoardPrivate {
drawing_area: gtk::DrawingArea,
current_player: Rc<RefCell<Color>>,
board: Rc<RefCell<BoardElement>>,
// board: Rc<RefCell<BoardElement>>,
cursor_location: Rc<RefCell<Option<Addr>>>,
api: Rc<RefCell<Option<CoreApi>>>,
@ -43,7 +42,7 @@ impl ObjectSubclass for BoardPrivate {
BoardPrivate {
drawing_area: Default::default(),
current_player: Rc::new(RefCell::new(Color::Black)),
board: Default::default(),
// board: Default::default(),
cursor_location: Default::default(),
api: Default::default(),
}
@ -55,7 +54,7 @@ impl ObjectImpl for BoardPrivate {
self.drawing_area.set_width_request(WIDTH);
self.drawing_area.set_height_request(HEIGHT);
let board = self.board.clone();
// let board = self.board.clone();
let cursor_location = self.cursor_location.clone();
let current_player = self.current_player.clone();
@ -85,6 +84,7 @@ impl ObjectImpl for BoardPrivate {
.set_draw_func(move |_, context, width, height| {
perftrace("render drawing area", || {
let render_start = std::time::Instant::now();
/*
let board = board.borrow();
match background {
@ -137,7 +137,9 @@ impl ObjectImpl for BoardPrivate {
pen.star_point(context, col, row);
});
});
*/
/*
(0..19).for_each(|col| {
(0..19).for_each(|row| {
if let IntersectionElement::Filled(stone) = board.stone(row, col) {
@ -162,6 +164,7 @@ impl ObjectImpl for BoardPrivate {
}
}
}
*/
let render_end = std::time::Instant::now();
println!("board rendering time: {:?}", render_end - render_start);
})
@ -169,10 +172,11 @@ impl ObjectImpl for BoardPrivate {
let motion_controller = gtk::EventControllerMotion::new();
{
let board = self.board.clone();
// let board = self.board.clone();
let cursor = self.cursor_location.clone();
let drawing_area = self.drawing_area.clone();
motion_controller.connect_motion(move |_, x, y| {
/*
let board = board.borrow();
let mut cursor = cursor.borrow_mut();
let hspace_between = ((WIDTH - 40) as f64) / ((board.size.width - 1) as f64);
@ -195,17 +199,21 @@ impl ObjectImpl for BoardPrivate {
*cursor = addr;
drawing_area.queue_draw();
}
*/
});
}
let gesture = gtk::GestureClick::new();
{
let board = self.board.clone();
// let board = self.board.clone();
let cursor = self.cursor_location.clone();
let api = self.api.clone();
gesture.connect_released(move |_, _, _, _| {
/*
let board = board.borrow();
let cursor = cursor.borrow();
*/
/*
match *cursor {
None => {}
Some(ref cursor) => {
@ -220,6 +228,7 @@ impl ObjectImpl for BoardPrivate {
}
}
}
*/
});
}
@ -244,10 +253,12 @@ impl Board {
s
}
/*
pub fn set_board(&self, board: BoardElement) {
*self.imp().board.borrow_mut() = board;
self.imp().drawing_area.queue_draw();
}
*/
pub fn set_current_player(&self, color: Color) {
*self.imp().current_player.borrow_mut() = color;

View File

@ -0,0 +1,265 @@
/*
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;
use glib::Object;
use gtk::{
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>>,
}
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) -> Self {
let s: Self = Object::builder().build();
*s.imp().board_state.borrow_mut() = board_state;
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 wood_texture = resources_lookup_data(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
gio::ResourceLookupFlags::NONE,
)
.unwrap();
let background = ImageReader::new(Cursor::new(wood_texture))
.with_guessed_format()
.unwrap()
.decode();
let background = background.map(|background| {
Pixbuf::from_bytes(
&glib::Bytes::from(background.as_bytes()),
gtk::gdk_pixbuf::Colorspace::Rgb,
false,
8,
background.width() as i32,
background.height() as i32,
background.to_rgb8().sample_layout().height_stride as i32,
)
.scale_simple(WIDTH, HEIGHT, InterpType::Nearest)
});
match background {
Ok(Some(ref background)) => {
ctx.set_source_pixbuf(background, 0., 0.);
ctx.paint().expect("paint should never fail");
}
Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7),
}
*/
ctx.set_source_rgb(0.7, 0.7, 0.7);
let _ = ctx.paint();
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 {
x_offset: MARGIN as f64,
y_offset: MARGIN as f64,
hspace_between,
vspace_between,
};
(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,
}
impl Pen {
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,
context: &cairo::Context,
row: u8,
col: u8,
color: Color,
liberties: Option<u8>,
) {
match color {
Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
};
self.draw_stone(context, 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, context: &cairo::Context, row: u8, col: u8, color: Color) {
match color {
Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5),
Color::Black => context.set_source_rgba(0.0, 0.0, 0.0, 0.5),
};
self.draw_stone(context, row, col);
}
fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
let radius = self.hspace_between / 2. - 2.;
let (x_loc, y_loc) = self.stone_location(row, col);
context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
let _ = context.fill();
}
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
(
self.x_offset + (col as f64) * self.hspace_between,
self.y_offset + (row as f64) * self.vspace_between,
)
}
}

View File

@ -2,12 +2,12 @@ use adw::{prelude::*, subclass::prelude::*};
use glib::Object;
use gtk::glib;
// use otg_core::ui::GamePreviewElement;
use sgf::Game;
use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
#[derive(Default)]
pub struct GameObjectPrivate {
game: Rc<RefCell<Option<Game>>>,
game: Rc<RefCell<Option<GameRecord>>>,
}
#[glib::object_subclass]
@ -23,13 +23,13 @@ glib::wrapper! {
}
impl GameObject {
pub fn new(game: Game) -> Self {
pub fn new(game: GameRecord) -> Self {
let s: Self = Object::builder().build();
*s.imp().game.borrow_mut() = Some(game);
s
}
pub fn game(&self) -> Option<Game> {
pub fn game(&self) -> Option<GameRecord> {
self.imp().game.borrow().clone()
}
}
@ -45,47 +45,16 @@ impl Default for LibraryPrivate {
let model = gio::ListStore::new::<GameObject>();
model.extend_from_slice(&vector);
/*
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let preview = GamePreview::new();
list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be a ListItem")
.set_child(Some(&preview));
});
factory.connect_bind(move |_, list_item| {
let game_element = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<GameObject>()
.expect("The item has to be a GameObject.");
let preview = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<GamePreview>()
.expect("The child has to be a GamePreview object.");
match game_element.game() {
Some(game) => preview.set_game(game),
None => (),
};
});
*/
let selection_model = gtk::NoSelection::new(Some(model.clone()));
let list_view = gtk::ColumnView::builder()
.model(&selection_model)
.single_click_activate(true)
.hexpand(true)
.build();
fn make_factory<F>(bind: F) -> gtk::SignalListItemFactory
where
F: Fn(Game) -> String + 'static,
F: Fn(GameRecord) -> String + 'static,
{
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(|_, list_item| {
@ -116,17 +85,7 @@ impl Default for LibraryPrivate {
.factory(&make_factory(|g| {
g.dates
.iter()
.map(|date| {
format!("{}", date)
/*
let l = locale!("en-US").into();
let options = length::Bag::from_date_style(length::Date::Medium);
let date = Date::try_new_iso_date(date.
let dtfmt =
DateFormatter::try_new_with_length(&l, options).unwrap();
dtfmt.format(date).unwrap()
*/
})
.map(|date| format!("{}", date))
.collect::<Vec<String>>()
.join(", ")
}))
@ -196,7 +155,29 @@ impl Default for Library {
}
impl Library {
pub fn set_games(&self, games: Vec<Game>) {
pub fn new(on_select: impl Fn(GameRecord) + 'static) -> Library {
let s = Library::default();
s.imp().list_view.connect_activate({
let s = s.clone();
move |_, row_id| {
println!("row activated: {}", row_id);
let object = s.imp().model.item(row_id);
let game = object.and_downcast_ref::<GameObject>().unwrap();
println!(
"{:?} vs. {:?}",
game.game().unwrap().white_player,
game.game().unwrap().black_player
);
on_select(game.game().unwrap());
}
});
s
}
pub fn set_games(&self, games: Vec<GameRecord>) {
let games = games
.into_iter()
.map(GameObject::new)

View File

@ -1,3 +1,22 @@
/*
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/>.
*/
// mod board;
// pub use board::Board;
// mod chat;
// pub use chat::Chat;
@ -7,11 +26,17 @@
// mod game_preview;
// pub use game_preview::GamePreview;
mod goban;
pub use goban::Goban;
mod library;
pub use library::Library;
// mod player_card;
// pub use player_card::PlayerCard;
mod player_card;
pub use player_card::PlayerCard;
mod review_tree;
pub use review_tree::ReviewTree;
// mod playing_field;
// pub use playing_field::PlayingField;
@ -19,9 +44,6 @@ pub use library::Library;
// mod home;
// pub use home::Home;
// mod board;
// pub use board::Board;
#[cfg(feature = "screenplay")]
pub use playing_field::playing_field_view;

View File

@ -1,11 +1,12 @@
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use otg_core::ui::PlayerCardElement;
use otg_core::Color;
use sgf::Player;
#[derive(Default)]
pub struct PlayerCardPrivate {
player_name: gtk::Label,
clock: gtk::Label,
// player_name: gtk::Label,
// clock: gtk::Label,
}
#[glib::object_subclass]
@ -24,16 +25,31 @@ glib::wrapper! {
}
impl PlayerCard {
pub fn new(element: PlayerCardElement) -> PlayerCard {
pub fn new(
color: Color,
player: &Player, /* add on current number of captures and time remaining on the clock*/
) -> PlayerCard {
let s: Self = Object::builder().build();
// I forget really the BEM system, but I know that what I want here is the block name and
// the modifier. I don't need an element here. So I'm going to mock it up and look up the
// real standard in my notes later.
match color {
Color::Black => s.set_css_classes(&["player-card", "player-card___black"]),
Color::White => s.set_css_classes(&["player-card", "player-card___white"]),
}
s.set_orientation(gtk::Orientation::Vertical);
/*
s.imp()
.player_name
.set_text(&format!("{} ({})", element.name, element.rank));
s.imp().clock.set_text(&element.clock);
*/
// s.imp().clock.set_text(&element.clock);
s.append(&s.imp().player_name);
s.append(&s.imp().clock);
let name: String = player.name.clone().unwrap_or(color.to_string());
let player_name = gtk::Label::new(Some(&name));
s.append(&player_name);
// s.append(&s.imp().clock);
s
}
}

View File

@ -0,0 +1,233 @@
/*
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/>.
*/
use cairo::Context;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Tree;
use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
use uuid::Uuid;
const WIDTH: i32 = 200;
const HEIGHT: i32 = 800;
#[derive(Default)]
pub struct ReviewTreePrivate {
record: Rc<RefCell<Option<GameRecord>>>,
tree: Rc<RefCell<Option<Tree<Uuid>>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ReviewTreePrivate {
const NAME: &'static str = "ReviewTree";
type Type = ReviewTree;
type ParentType = gtk::DrawingArea;
}
impl ObjectImpl for ReviewTreePrivate {}
impl WidgetImpl for ReviewTreePrivate {}
impl DrawingAreaImpl for ReviewTreePrivate {}
glib::wrapper! {
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) @extends gtk::Widget, gtk::DrawingArea;
}
impl ReviewTree {
pub fn new(record: GameRecord) -> Self {
let s: Self = Object::new();
*s.imp().tree.borrow_mut() = Some(Tree::from(&record.children[0]));
*s.imp().record.borrow_mut() = Some(record);
s.set_width_request(WIDTH);
s.set_height_request(HEIGHT);
s.set_draw_func({
let s = s.clone();
move |_, ctx, width, height| {
s.redraw(ctx, width, height);
}
});
s
}
pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
let tree: &Option<Tree<Uuid>> = &self.imp().tree.borrow();
match tree {
Some(ref tree) => {
for node in tree.bfs_iter() {
// draw a circle given the coordinates of the nodes
// I don't know the indent. How do I keep track of that? Do I track the position of
// the parent? do I need to just make it more intrinsically a part of the position
// code?
ctx.set_source_rgb(0.7, 0.7, 0.7);
let (row, column) = tree.position(node.id);
let y = (row as f64) * 20. + 10.;
let x = (column as f64) * 20. + 10.;
ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI);
let _ = ctx.stroke();
}
}
None => {
// if there is no tree present, then there's nothing to draw!
}
}
}
}
// https://llimllib.github.io/pymag-trees/
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
// their diagram, they got a tree that looks like this.
//
// O
// |\
// O O
// |\ \ \
// O O O O
// |\ |\
// O O O O
//
// In the same circumstance, what I want is this:
//
// O--
// | \
// O O
// |\ |\
// O O O O
// |\
// O O
//
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
// with another branch, there is some extra drawing space. This might actually be similar to adding
// the principal that "A parent should be centered over its children".
//
// So, given a tree, I need to know how many children exist at each level. Then I build parents
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
// the graph.
//
// A bottom-up traversal:
// - Figure out the number of nodes at each depth
/*
struct Tree {
width: Vec<usize>, // the total width of the tree at each depth
}
*/
#[cfg(test)]
mod test {
use super::*;
use sgf::{Color, GameNode, Move, MoveNode};
#[test]
fn it_calculates_width_for_single_node() {
let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
assert_eq!(node_width(&node), 1);
}
#[test]
fn it_calculates_width_for_node_with_children() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
node_a.children.push(node_b);
node_a.children.push(node_c);
node_a.children.push(node_d);
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
}
// A
// B E
// C D
#[test]
fn it_calculates_width_with_one_deep_child() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
node_b.children.push(GameNode::MoveNode(node_c));
node_b.children.push(GameNode::MoveNode(node_d));
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2);
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_e));
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_calculates_a_complex_tree() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3);
node_b.children.push(GameNode::MoveNode(node_c));
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3);
node_h.children.push(GameNode::MoveNode(node_i));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
// This should be 4 if I were collapsing levels correctly, but it is 5 until I return to
// figure that step out.
assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5);
}
#[test]
fn a_nodes_children_get_separate_columns() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
node_a.children.push(node_b.clone());
node_a.children.push(node_c.clone());
node_a.children.push(node_d.clone());
assert_eq!(
node_children_columns(&GameNode::MoveNode(node_a)),
vec![0, 1, 2]
);
}
#[test]
fn text_renderer() {
assert!(false);
}
}

View File

@ -21,10 +21,10 @@ pub use app_window::AppWindow;
mod views;
use async_std::task::{spawn, yield_now};
use async_std::task::{yield_now};
use otg_core::{Core, Observable, CoreRequest, CoreResponse};
use std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime;
use std::{rc::Rc};
#[derive(Clone)]
pub struct CoreApi {
@ -51,6 +51,7 @@ where
/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
///
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.
#[allow(dead_code)]
struct LocalObserver<T> {
join_handle: glib::JoinHandle<()>,
handler: Rc<dyn Fn(T)>,
@ -61,6 +62,7 @@ impl<T: 'static> LocalObserver<T> {
///
/// observable -- any object which emits events
/// handler -- a function which can process events
#[allow(dead_code)]
fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self {
let listener = observable.subscribe();
let handler = Rc::new(handler);

View File

@ -4,17 +4,15 @@ use async_std::task::spawn;
use gio::ActionEntry;
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
use otg_gtk::{
perftrace,
// ui::{ConfigurationPage, Home, PlayingField},
AppWindow,
CoreApi,
};
use std::sync::{Arc, RwLock};
const APP_ID_DEV: &str = "com.luminescent-dreams.otg.dev";
const APP_ID_PROD: &str = "com.luminescent-dreams.otg";
const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/otg/";
// const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/otg/";
async fn handler(notifications: Receiver<CoreNotification>, app_id: String) {
loop {
@ -108,7 +106,7 @@ fn main() {
APP_ID_PROD
};
let config = load_config(&app_id);
let config = load_config(app_id);
let core = Core::new(config.clone());

View File

@ -0,0 +1,100 @@
/*
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/>.
*/
// Game review consists of the board, some information about the players, the game tree, and any
// commentary on the current move. This requires four major components, some of which are easier
// than others. The game board has to be kept in sync with the game tree, so there's a
// communication channel there.
//
// I'll get all of the information about the game from the core, and then render everything in the
// UI. So this will be a heavy lift on the UI side.
use crate::{components::{Goban, PlayerCard, ReviewTree}, CoreApi};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Color;
use sgf::GameRecord;
#[derive(Default)]
pub struct GameReviewPrivate {}
#[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate {
const NAME: &'static str = "GameReview";
type Type = GameReview;
type ParentType = gtk::Box;
}
impl ObjectImpl for GameReviewPrivate {}
impl WidgetImpl for GameReviewPrivate {}
impl BoxImpl for GameReviewPrivate {}
glib::wrapper! {
pub struct GameReview(ObjectSubclass<GameReviewPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Accessible;
}
impl GameReview {
pub fn new(_api: CoreApi, record: GameRecord) -> Self {
let s: Self = Object::builder().build();
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
// This is not a fatal error, so I'll replace this `unwrap` call with something that
// renders the board and notifies the user of a problem that cannot be resolved.
let board_repr = otg_core::Goban::default()
.apply_moves(record.mainline())
.unwrap();
let board = Goban::new(board_repr);
/*
s.attach(&board, 0, 0, 2, 2);
s.attach(&gtk::Label::new(Some("white player")), 0, 2, 1, 1);
s.attach(&gtk::Label::new(Some("black player")), 0, 2, 1, 2);
s.attach(&gtk::Label::new(Some("chat")), 1, 2, 2, 2);
*/
let sidebar = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.build();
// The review tree needs to know the record for being able to render all of the nodes. Once
// keyboard input is being handled, the tree will have to be updated on each keystroke in
// order to show the user where they are within the game record.
let review_tree = ReviewTree::new(record.clone());
// I think most keyboard focus is going to end up being handled here in GameReview, as
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
// also the player information, seeing as moving to a new position may mean that the score
// and time remaining can be updated.
let player_information_section = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(4)
.build();
player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player));
player_information_section.append(&PlayerCard::new(Color::White, &record.white_player));
s.append(&board);
sidebar.append(&player_information_section);
sidebar.append(&review_tree);
s.append(&sidebar);
s
}
}

View File

@ -21,7 +21,8 @@ use otg_core::{
library::{LibraryRequest, LibraryResponse},
CoreRequest, CoreResponse,
};
use std::{cell::RefCell, rc::Rc};
use sgf::GameRecord;
/*
struct PlayerDataEntryPrivate {
@ -100,19 +101,13 @@ impl PlayerDataEntry {
}
*/
#[derive(Default)]
pub struct HomePrivate {
// black_player: Rc<RefCell<Option<PlayerDataEntry>>>,
// white_player: Rc<RefCell<Option<PlayerDataEntry>>>,
}
impl Default for HomePrivate {
fn default() -> Self {
Self {
// black_player: Rc::new(RefCell::new(None)),
// white_player: Rc::new(RefCell::new(None)),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for HomePrivate {
@ -126,11 +121,11 @@ impl WidgetImpl for HomePrivate {}
impl BoxImpl for HomePrivate {}
glib::wrapper! {
pub struct HomeView(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
pub struct HomeView(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable, gtk::Accessible;
}
impl HomeView {
pub fn new(api: CoreApi) -> Self {
pub fn new(api: CoreApi, on_select_game: impl Fn(GameRecord) + 'static) -> Self {
let s: Self = Object::builder().build();
s.set_spacing(4);
s.set_homogeneous(false);
@ -157,7 +152,7 @@ impl HomeView {
s.append(&new_game_button);
*/
let library = Library::default();
let library = Library::new(on_select_game);
let library_view = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.min_content_width(360)

View File

@ -1,3 +1,22 @@
/*
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/>.
*/
mod game_review;
pub use game_review::GameReview;
mod home;
pub use home::HomeView;

View File

@ -14,11 +14,11 @@ 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/>.
*/
use std::{borrow::Cow, cell::RefCell, path::Path, rc::Rc};
use std::{cell::RefCell, rc::Rc};
use adw::prelude::*;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use gtk::{subclass::prelude::*};
use otg_core::{Config, ConfigOption, LibraryPath};
fn library_chooser_row(
@ -33,7 +33,7 @@ fn library_chooser_row(
.valign(gtk::Align::Center)
.build();
let parent = parent.clone();
let _parent = parent.clone();
let library_row = adw::ActionRow::builder()
.title("Library Path")

View File

@ -11,9 +11,5 @@ fn main() {
println!("{:?}", file);
let games = parse_sgf_file(&file).unwrap();
for sgf in games {
if let Ok(sgf) = sgf {
println!("{:?}", sgf.white_player);
}
}
games.into_iter().flatten().for_each(|sgf| println!("{:?}", sgf.white_player));
}

View File

@ -49,11 +49,11 @@ pub struct Player {
/// level, the interpreter will reject any games that have setup properties and move properties
/// mixed in a single node. If there are other semantic problems, the interpreter will reject
/// those, as well. Where the function of the parser is to understand and correct fundamental
/// syntax issues, the result of the Game is to have a fully-understood game. However, this doesn't
/// (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this
/// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
/// whatever).
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Game {
pub struct GameRecord {
pub game_type: GameType,
// TODO: board size is not necessary in all games. Hive has no defined board size.
@ -81,7 +81,7 @@ pub struct Game {
pub children: Vec<GameNode>,
}
impl Game {
impl GameRecord {
pub fn new(
game_type: GameType,
board_size: Size,
@ -114,20 +114,49 @@ impl Game {
children: vec![],
}
}
/// Generate a list of moves which constitute the main line of the game. This is the game as it
/// was actually played out, and by convention consists of the first node in each list of
/// children.
pub fn mainline(&self) -> Vec<&GameNode> {
let mut moves: Vec<&GameNode> = vec![];
let mut next = self.children.first();
while let Some(node) = next {
// Given that I know that I have a node, and I know that I'm going to push a reference
// to it onto my final list, I want to get the first of its children. And I want to
// keep doing that until there are no more first children.
//
// Just going to push references onto the list. No need to copy the nodes for this.
//
// Pushing a reference onto the list implicitely clones the reference, but not the data
// it is pointing to. This means that each time through the loop, `next` points to
// something else. This isn't being described very well, though, so it's worth
// reviewing in the future.
moves.push(node);
next = match node {
GameNode::MoveNode(node) => node.children.first(),
GameNode::SetupNode(node) => node.children.first(),
};
}
moves
}
}
impl Node for Game {
impl Node for GameRecord {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode {
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
self.children.push(node);
self.children.last_mut().unwrap()
}
}
impl TryFrom<&parser::Tree> for Game {
impl TryFrom<&parser::Tree> for GameRecord {
type Error = GameError;
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> {
@ -198,7 +227,7 @@ impl TryFrom<&parser::Tree> for Game {
parser::Property::Round(v) => s.round = Some(v.clone()),
parser::Property::Ruleset(v) => s.rules = Some(v.clone()),
parser::Property::Source(v) => s.source = Some(v.clone()),
parser::Property::TimeLimit(v) => s.time_limit = Some(v.clone()),
parser::Property::TimeLimit(v) => s.time_limit = Some(*v),
parser::Property::Overtime(v) => s.overtime = Some(v.clone()),
// parser::Property::Data(v) => s.transcriber = Some(v.clone()),
_ => {}
@ -209,7 +238,7 @@ impl TryFrom<&parser::Tree> for Game {
.root
.next
.iter()
.map(|node| GameNode::try_from(node))
.map(GameNode::try_from)
.collect::<Result<Vec<GameNode>, GameNodeError>>()
.map_err(GameError::InvalidGameNode)?;
@ -228,18 +257,17 @@ pub trait Node {
fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
self.children()
.iter()
.map(|node| {
.flat_map(|node| {
let mut children = node.nodes();
let mut v = vec![*node];
v.append(&mut children);
v
})
.flatten()
.collect::<Vec<&'a GameNode>>()
}
fn children<'a>(&'a self) -> Vec<&'a GameNode>;
fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode;
fn children(&self) -> Vec<&GameNode>;
fn add_child(&mut self, node: GameNode) -> &mut GameNode;
}
impl GameNode {
@ -252,21 +280,21 @@ impl GameNode {
}
impl Node for GameNode {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
fn children(&self) -> Vec<&GameNode> {
match self {
GameNode::MoveNode(node) => node.children(),
GameNode::SetupNode(node) => node.children(),
}
}
fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
fn nodes(&self) -> Vec<&GameNode> {
match self {
GameNode::MoveNode(node) => node.nodes(),
GameNode::SetupNode(node) => node.nodes(),
}
}
fn add_child<'a>(&'a mut self, new_node: GameNode) -> &'a mut GameNode {
fn add_child(&mut self, new_node: GameNode) -> &mut GameNode {
match self {
GameNode::MoveNode(node) => node.add_child(new_node),
GameNode::SetupNode(node) => node.add_child(new_node),
@ -297,7 +325,7 @@ impl TryFrom<&parser::Node> for GameNode {
let children = n
.next
.iter()
.map(|n| GameNode::try_from(n))
.map(GameNode::try_from)
.collect::<Result<Vec<Self>, Self::Error>>()?;
let node = match (move_node, setup_node) {
@ -320,19 +348,19 @@ impl TryFrom<&parser::Node> for GameNode {
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct MoveNode {
id: Uuid,
color: Color,
mv: Move,
children: Vec<GameNode>,
pub id: Uuid,
pub color: Color,
pub mv: Move,
pub children: Vec<GameNode>,
time_left: Option<Duration>,
moves_left: Option<usize>,
name: Option<String>,
evaluation: Option<Evaluation>,
value: Option<f64>,
comments: Option<String>,
annotation: Option<Annotation>,
unknown_props: Vec<(String, String)>,
pub time_left: Option<Duration>,
pub moves_left: Option<usize>,
pub name: Option<String>,
pub evaluation: Option<Evaluation>,
pub value: Option<f64>,
pub comments: Option<String>,
pub annotation: Option<Annotation>,
pub unknown_props: Vec<(String, String)>,
}
impl MoveNode {
@ -360,7 +388,7 @@ impl Node for MoveNode {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode {
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
self.children.push(node);
self.children.last_mut().unwrap()
}
@ -388,7 +416,7 @@ impl TryFrom<&parser::Node> for MoveNode {
if s.time_left.is_some() {
return Err(Self::Error::ConflictingProperty);
}
s.time_left = Some(duration.clone());
s.time_left = Some(*duration);
}
parser::Property::Comment(cmt) => {
if s.comments.is_some() {
@ -431,8 +459,8 @@ impl TryFrom<&parser::Node> for MoveNode {
pub struct SetupNode {
id: Uuid,
positions: Vec<parser::SetupInstr>,
children: Vec<GameNode>,
pub positions: Vec<parser::SetupInstr>,
pub children: Vec<GameNode>,
}
impl SetupNode {
@ -463,7 +491,7 @@ impl Node for SetupNode {
}
#[allow(dead_code)]
fn add_child<'a>(&'a mut self, _node: GameNode) -> &'a mut GameNode {
fn add_child(&mut self, _node: GameNode) -> &mut GameNode {
unimplemented!()
}
}
@ -480,7 +508,7 @@ impl TryFrom<&parser::Node> for SetupNode {
}
#[allow(dead_code)]
pub fn path_to_node<'a>(node: &'a GameNode, id: Uuid) -> Vec<&'a GameNode> {
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
if node.id() == id {
return vec![node];
}
@ -503,7 +531,7 @@ mod test {
#[test]
fn it_can_create_an_empty_game_tree() {
let tree = Game::new(
let tree = GameRecord::new(
GameType::Go,
Size {
width: 19,
@ -517,7 +545,7 @@ mod test {
#[test]
fn it_can_add_moves_to_a_game() {
let mut game = Game::new(
let mut game = GameRecord::new(
GameType::Go,
Size {
width: 19,
@ -666,6 +694,72 @@ mod setup_node_tests {
#[cfg(test)]
mod path_test {
use super::*;
use cool_asserts::assert_matches;
use parser::parse_collection;
use std::{fs::File, io::Read};
fn with_text(text: &str, f: impl FnOnce(Vec<GameRecord>)) {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
.collect::<Vec<GameRecord>>();
f(games);
}
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<GameRecord>)) {
let mut file = File::open(path).unwrap();
let mut text = String::new();
let _ = file.read_to_string(&mut text);
with_text(&text, f);
}
#[test]
fn returns_the_mainline_of_a_game_without_branches() {
with_file(
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|games| {
let game = &games[0];
let moves = game.mainline();
assert_matches!(moves[0], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pp".to_owned()));
});
assert_matches!(moves[1], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dp".to_owned()));
});
assert_matches!(moves[2], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pd".to_owned()));
});
},
)
}
#[test]
fn returns_the_mainline_of_a_game_with_branches() {
with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| {
let game = &games[0];
let moves = game.mainline();
assert_matches!(moves[1], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dd".to_owned()));
});
assert_matches!(moves[2], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("op".to_owned()));
});
assert_matches!(moves[3], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dp".to_owned()));
});
});
}
#[ignore]
#[test]
fn returns_empty_list_if_no_game_nodes() {
@ -693,16 +787,16 @@ mod file_test {
use parser::parse_collection;
use std::{fs::File, io::Read};
fn with_text(text: &str, f: impl FnOnce(Vec<Game>)) {
fn with_text(text: &str, f: impl FnOnce(Vec<GameRecord>)) {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games
.into_iter()
.map(|game| Game::try_from(&game).expect("game to parse"))
.collect::<Vec<Game>>();
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
.collect::<Vec<GameRecord>>();
f(games);
}
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<Game>)) {
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<GameRecord>)) {
let mut file = File::open(path).unwrap();
let mut text = String::new();
let _ = file.read_to_string(&mut text);

View File

@ -1,14 +1,18 @@
mod date;
mod game;
pub use game::{GameNode, GameRecord, MoveNode, Player};
mod parser;
pub use parser::{parse_collection, Move};
mod types;
pub use types::*;
pub use date::Date;
use std::{fs::File, io::Read};
pub use date::Date;
pub use game::Game;
pub use parser::parse_collection;
use thiserror::Error;
pub use types::*;
#[derive(Debug)]
pub enum Error {
@ -58,16 +62,29 @@ impl From<nom::error::Error<&str>> for ParseError {
}
}
pub fn parse_sgf(input: &str) -> Result<Vec<Result<Game, game::GameError>>, Error> {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(&input)?;
let games = games.into_iter()
.map(|game| Game::try_from(&game))
.collect::<Vec<Result<Game, game::GameError>>>();
/// Given raw text of an SGF file, parse all of the games within that file.
///
/// The outermost Result is for any errors that happen in opening and reading the file, or if hte
/// outermost part of the file format is invalid.
///
/// The inner Result is for errors in each individual game in the file. All of the other games can
/// still be kept as valid.
pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game))
.collect::<Vec<Result<GameRecord, game::GameError>>>();
Ok(games)
}
pub fn parse_sgf_file(path: &std::path::Path) -> Result<Vec<Result<Game, game::GameError>>, Error> {
/// Given a path, parse all of the games stored in that file.
///
/// See also `parse_sgf`
pub fn parse_sgf_file(
path: &std::path::Path,
) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> {
let mut file = File::open(path).unwrap();
let mut text = String::new();
let _ = file.read_to_string(&mut text);

View File

@ -10,7 +10,7 @@ use nom::{
IResult, Parser,
};
use serde::{Deserialize, Serialize};
use std::{num::ParseIntError, time::Duration};
use std::{fmt::Write, num::ParseIntError, time::Duration};
impl From<ParseSizeError> for Error {
fn from(_: ParseSizeError) -> Self {
@ -295,6 +295,26 @@ pub enum Move {
Pass,
}
impl Move {
pub fn coordinate(&self) -> Option<(u8, u8)> {
match self {
Move::Pass => None,
Move::Move(s) => {
if s.len() == 2 {
let mut parts = s.chars();
let row_char = parts.next().unwrap();
let row = row_char as u8 - b'a';
let column_char = parts.next().unwrap();
let column = column_char as u8 - b'a';
Some((row, column))
} else {
unimplemented!("moves must contain exactly two characters");
}
}
}
}
}
// KO
// MN
// N
@ -507,22 +527,20 @@ impl ToString for Property {
Property::WhiteRank(value) => format!("WR[{}]", value),
Property::WhiteTeam(value) => format!("WT[{}]", value),
Property::Territory(Color::White, positions) => {
format!(
"TW{}",
positions
.iter()
.map(|Position(p)| format!("[{}]", p))
.collect::<String>()
)
positions
.iter()
.fold("TW".to_owned(), |mut output, Position(p)| {
let _ = write!(output, "{}", p);
output
})
}
Property::Territory(Color::Black, positions) => {
format!(
"TB{}",
positions
.iter()
.map(|Position(p)| format!("[{}]", p))
.collect::<String>()
)
positions
.iter()
.fold("TB".to_owned(), |mut output, Position(p)| {
let _ = write!(output, "{}", p);
output
})
}
Property::Unknown(UnknownProperty { ident, value }) => {
format!("{}[{}]", ident, value)
@ -945,7 +963,7 @@ fn parse_win_score<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a
mod test {
use super::*;
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
const EXAMPLE: &str = "(;FF[4]C[root](;C[a];C[b](;C[c])
(;C[d];C[e]))
(;C[f](;C[g];C[h];C[i])
(;C[j])))";
@ -1184,6 +1202,14 @@ k<. Hard line breaks are all other linebreaks.",
);
parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap();
}
#[test]
fn it_can_convert_moves_to_coordinates() {
assert_eq!(Move::Pass.coordinate(), None);
assert_eq!(Move::Move("dd".to_owned()).coordinate(), Some((3, 3)));
assert_eq!(Move::Move("jj".to_owned()).coordinate(), Some((9, 9)));
assert_eq!(Move::Move("pp".to_owned()).coordinate(), Some((15, 15)));
}
}
#[cfg(test)]

View File

@ -0,0 +1,923 @@
(;FF[4]CA[UTF-8]GM[1]GN[Michael Redmond 9p vs Otake Hideo 9p]PC[https://online-go.com/review/406670]PB[Otake Hideo]PW[Michael Redmond]BR[9p]WR[9p]TM[0]OT[0 none]RE[?]SZ[19]KM[6.5]RU[Japanese]C[
-- chat --
S_Alexander: Hello. This is live relay from wbaduk. For the Glory of Kibitz.
S_Alexander: Michael Redmond 9p vs Otake Hideo 9p
S_Alexander: 68th Oza title preliminaries B
S_Alexander: Time: 3 hours each and byo-yomi
S_Alexander: Otake Hideo holds Honorary Gosei title
]AP[Sabaki:0.43.3];B[qd];W[dp];B[pq];W[dd];B[fc];W[oc];B[cf];W[df];B[dg];W[ef];B[ce];W[cd];B[gd];W[eb];B[kc]C[
-- chat --
S_Alexander: Old people love double 3-4, huh.
jager: everyone loves shusaku
BHydden: Shusaku is bae
];W[di];B[eg];W[gf];B[fg];W[ge]C[
-- chat --
IAMTRADER: 4-4 for white is simple and strong
IAMTRADER: in my opinion 4-4 for black is a bit waste // remember that you got 1 stone ahead you can use it to enclosure the corner or approach
];B[ch];W[ic]C[
-- chat --
IAMTRADER: 3-4 still playable in this era, just knowing what you doing thats all
S_Alexander: just funny. it was the same last game. older opponent with double 3-4. and redmond with hoshis
jager: maybe they want to show us the old ways are still fun to play :)
jager: two games in arow with the double approach 4-4 after 3-4 too
S_Alexander: next redmond's game is august 8th i think
BHydden: 4-4 for either player is simple and strong
jager: i really like otake too i have several of his books and his games are famous for good shapes
S_Alexander: what happens next? fighting?
jager: i like dual 3-4 as black still but all the ai play 4-4
];B[hd];W[jd];B[de]C[
-- chat --
S_Alexander: anyone has lz?
vastias: I'd never expected H16 :O
IAMTRADER: yeah normally we would do J16 stuff
vastias: yup
IAMTRADER: H16 also strengthen black for attacking white at the top left
IAMTRADER: super amazing
];W[ee];B[ed];W[ec];B[fd]C[
-- chat --
S_Alexander: everything's so weak
floobulous: just like my forearms..
IAMTRADER: lol
IAMTRADER: 1. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16
IAMTRADER: either way there will be problem for white
];W[if]C[
-- chat --
S_Alexander: fixing outside
S_Alexander: very thin in any case
IAMTRADER: or c18
IAMTRADER: yeah
S_Alexander: i imagine black plays in the corner first to see what white does
tonybe: thank you again S_Alexander! you rock!
](;B[cb]C[
-- chat --
S_Alexander: you're welcome 😊
AquaBot: Anyone want AI analysis?
S_Alexander: I want!
scotto: maybe white gives up corner and 2 stones to capture 5
AquaBot: I will be analysing with Leelamaster, a Leela Zero hybrid with human games, to more accurately guess what human pros will play
scotto: thx
tonybe: nice!
](;W[dc];B[fb]C[
-- chat --
jillgoodgal: all right a pro game starting right off with fighting!
S_Alexander: scary
IAMTRADER: so michael decide to connect
IAMTRADER: get rid of all problem.. just for now
];W[bc];B[bb]C[
-- chat --
AquaBot: B winrate 57% here
AquaBot: at move 35
BHydden: If white doesn't live there black gets a 30 point corner... Is that even recoverable for white?
AquaBot: W will live there, plenty of options
BHydden: Wow bot says more even than I thought
BHydden: Yeah? I'll enjoy seeing that
OpenTome: FOR THE GLORY OF KI-BLITZ!
jillgoodgal: many liberties
](;W[be];B[bf];W[ab];B[ae]C[
-- chat --
AquaBot: They're playing my first variaton
];W[bd](;B[fa]C[
-- chat --
AquaBot: on variation 4?
];W[ea];B[ib]C[
-- chat --
AquaBot: options.. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18
AquaBot: Variation 6 was best move however
](;W[kd];B[jb]C[
-- chat --
AquaBot: Options are M17 37.5
AquaBot: Q15 42 winrate
jillgoodgal: time to attack the bottom right stone I think soon
AquaBot: C11 40 winrate
];W[lc];B[lb];W[jc];B[kb]C[
-- chat --
AquaBot: W winrate 34 now
AquaBot: Which variation would you like to see?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18
S_Alexander: Fight, Redmond!
];W[pe];B[qe];W[pg](;B[pf];W[of];B[qf];W[og];B[qh];W[ci];B[fi]C[
-- chat --
jillgoodgal: R5
S_Alexander: Wow, what now?
];W[cm];B[hh]C[
-- chat --
S_Alexander: 1 hour into the game
];W[ih];B[ii];W[ji];B[ig];W[jh];B[hg];W[jg];B[mc]C[
-- chat --
finetype: Can someone explain (1) how aquabot works/what is going on? (2) what is the state of that top left white group? I'm baffled at it being left alone, is it just accepted that it's dead? A16 looks so important to me, I feel pretty dumb right now because I'm clearly missing something huge.
finetype: how much time is on their clock? Is there any way to know?
];W[qo];B[me]C[
-- chat --
S_Alexander: no idea either
];W[ij];B[hi]C[
-- chat --
BHydden: I'm guessing top left is worst case scenario either seki or ko, and probably white doesn't have time to come back and live outright in gote
S_Alexander: poor redmond
bgauch: it looks like redmond got a lot on the left side, and he has komi. I wouldnt count him out yet :)
barelydan: it's alive
barelydan: well some thousand year ko thing?
BHydden: lots
BHydden: they started with 3 hrs each and the game has only been going for an hour
];W[np];B[po]C[
-- chat --
jillgoodgal: hah I called that R5 move a while ago!
go for the gun: why o4 over p4?
AquaBot: w WINRATE 40
S_Alexander: not bad yet
liminal: The bottom left is not yet explored, or even touched by black. I suspect the o4 move is a nod to the idea that victory for white will be found in the lower left.
go for the gun: i guess since there is still so much space on the bottom and right the looseness of o4 is not necessarily damaging
savanni.dgerinel: I also feel like O4 will block black's entry into the lower left. At P4, black could simply stretch at P3, and then it's harder to contain.
jillgoodgal: R6 now
liminal: Because pincering at m4 for black would be a "bad idea" but o3 is still "attacking" the bottom right.
](;W[pn];B[oo]C[
-- chat --
jillgoodgal: or next to it
];W[qp]C[
-- chat --
liminal: I don't think so.
liminal: More likely to go under.
MeiGuoTang: I dont redmond has good winning record against the older pros, that were prominent in back in their time
liminal: That last variation seems more in line with what White would want.
](;B[oq];W[qq];B[kq]C[
-- chat --
go for the gun: black will pincer o4
];W[nm]C[
-- chat --
go for the gun: to attack bottom
jillgoodgal: pincer anyway
](;B[no];W[lp]C[
-- chat --
S_Alexander: redmond won last game against otake with black by 1.5
S_Alexander: in 2016-ish i think
MeiGuoTang: ....
MeiGuoTang: liminal
MeiGuoTang: there was no influence
MeiGuoTang: no
MeiGuoTang: that corner would be to big
MeiGuoTang: no
go for the gun: white doesnt really have the corner yet
MeiGuoTang: white currently has 9 points at s4
go for the gun: sorry, i thought you were talking about bottom left corner
AlexanderKratz: doesnt h14 cut white?
MeiGuoTang: what are you saying?
AquaBot: Optimal gameplay from here https://i.imgur.com/gnzp3ZX.jpg
bgauch: liminal I think you got the coordinates wrong
](;B[lq](;W[qj];B[cq];W[cp];B[dq];W[eq];B[er]C[
-- chat --
go for the gun: that is assuming a lot
];W[fr];B[fq]C[
-- chat --
S_Alexander: jesus they're suddenly fast
AquaBot: W loist 5 percentage points by playing E3 instead of F3
];W[ep];B[gr];W[dr];B[fs];W[cr];B[ie]C[
-- chat --
bgauch: S_Alexander I think it is popular new-ish 3-3 invasion joseki, so they both played it fast
bgauch: yes, I keep having to refresh the page for some reason
];W[jf];B[hf]C[
-- chat --
AquaBot: B winrate 75
];W[go];B[lf];W[hq];B[iq];W[ip];B[ir];W[qc];B[rc]C[
-- chat --
MeiGuoTang: what do you mean q3 and r3 are captured?
liminal: Wait, is the main line NOT the current line?
S_Alexander: all caught up i think
go for the gun: liminal, maybe you should refresh page lol
AquaBot: B winrate 76
go for the gun: i got stuck in a strange line earlier
liminal: What is the move number?
];W[qg]C[
-- chat --
go for the gun: 109
];B[rg]C[
-- chat --
MeiGuoTang: 110
];W[pd]C[
-- chat --
jillgoodgal: 110
MeiGuoTang: 11
MeiGuoTang: 123
MeiGuoTang: guys
S_Alexander: i mean all the moves, not only joseki
];B[qb]C[
-- chat --
MeiGuoTang: just click sync
];W[pc]C[
-- chat --
savanni.dgerinel: There's frequently a synchronize button that will put you back in line with the PetAccount.
];B[re];W[ri];B[rh];W[oi]C[
-- chat --
AquaBot: 13. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11
MeiGuoTang: there a sync button for a reason
liminal: Why isn't the first line the actual game?
MeiGuoTang: 15. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11
jillgoodgal: it is a demo board
liminal: I know it's a demo boad.
liminal: Not th epoint.
go for the gun: i think that's the point
jillgoodgal: how it works
MeiGuoTang: liminal
floobulous: black 70% win.. time to give up
liminal: yes?
MeiGuoTang: it is the first line in the demo
MeiGuoTang: PetAccount hasnt deviated any moves
jillgoodgal: definitely reminds me of my games, no place to develop
MeiGuoTang: when you click a varation that continues the line it disrupts "the main" for you and you only
liminal: reloading...
liminal: Okay, sorry for the confusion.
bgauch: I like this position for white, looks like a lot of potential on the right of the center now. :)
S_Alexander: redmond likes influence
jillgoodgal: lots of space but lots of black lines can enter
MeiGuoTang: k15 is worth 20 points
](;B[ne];W[oe];B[qi];W[rj](;B[mh]C[
-- chat --
bgauch: black stones can invade from around M14 but I would not be worried about an invasion from P5 or R12 stones here, I dont think they can come in to the center
MeiGuoTang: I wouldnt play it right now
MeiGuoTang: but worth keeping in mind
MeiGuoTang: its gote for both players
];W[mg]C[
-- chat --
S_Alexander: redmond is good at endgame
S_Alexander: can he overcome disadvantage there?
MeiGuoTang: if otake is comfortable I doubt it
AquaBot: B winrate 67 here
](;B[mm]C[
-- chat --
MeiGuoTang: "best" move
MeiGuoTang: im pretty sure a move at q8 would lose white the game
bgauch: I would play N8 on instinct here; but perhaps redmond has plans for his aji at M4 and O4
AquaBot: N8 N6 are ok moves but M13 bet move
AquaBot: Yeah M13
bgauch: I havent read out the important fight over K15, but perhaps M13 helps with that? hmm
MeiGuoTang: no it doesnt
jillgoodgal: oops I was accidently on a varition sorry
S_Alexander: hmmm, thinking
bgauch: thanks for the variation MeiGuoTang. white does look very dead; I suppose the meaning of M13 is just to hold on to more of the center, and probably kill the one black stone.
MeiGuoTang: I dont think redmond will play m13
AquaBot: I dont think so either
jillgoodgal: you think N6?
Wong Kar-Wai: Lunch?
bgauch: N8 seems simpler and is the move I would play, but N6 looks pro to a weak player like me - it looks like it is doing something with the aji but I cant quite see where it leads. If Redmond is spending all this time thinking, maybe it is about N6.
jillgoodgal: certainly seems nice to have that 13 line
jillgoodgal: with the M13 play
bgauch: why would anyone play M16 and not K15?
MeiGuoTang: I thought m16 as a sac
MeiGuoTang: but black doesnt need to capture to connect
MeiGuoTang: so yes it is bad
bgauch: well, at least M16 would be sente, right? So in one my games, that would be a "small" mistake compared to my other ones :)
gemm: so what should w play?
bgauch: gemm AI says M13, but N6 and N8 are also good
bgauch: or "OK" at least
jillgoodgal: I am betting on N6
gemm: yeah I looked at the variations
carrootjazz: is this game live now?
AquaBot: I did variations for all N6, N8 and M13
gemm: still unconfortable to play m13
gemm: yes I saw them Aqua
AquaBot: Here is a cool move...
carrootjazz: live or kifu ??
jillgoodgal: live relay
S_Alexander: i think they're on break now and will continue on 30 minutes
carrootjazz: thx @jillgoodgal
jillgoodgal: no 2 liberties to 1
jillgoodgal: in the last variation
jillgoodgal: presumably Michael needs to set up some ko battles for endgame?
MeiGuoTang: out side seems more important I would think
omeara: Is the actual game on Move 125? i'm a bit confused with the variations coming off the main line
MeiGuoTang: omeara
MeiGuoTang: click sync
MeiGuoTang: thats the game line
omeara: haha thank you
MeiGuoTang: pet hasnt added any varations
MeiGuoTang: you did
S_Alexander: also reload the page if you want
bgauch: where is "sync"? The pop-out menu on the right side? I dont see the option. Sorry if I'm just blind.
jillgoodgal: if you don't see the synch button you are synched most likely
omeara: It seems "sync" appears only if you've strayed away from the main game
jillgoodgal: N6 is still my bet yes
bgauch: ah, perhaps I am already synced then. Thanks.
MeiGuoTang: Game move. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7
MeiGuoTang: PetAccount must be lazy.. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7
jillgoodgal: once the game starts again you can get rid of the branches with synch
gemm: really Mei?
MeiGuoTang: you can just deleted the varations or refresh the page
BHydden: he's going to the trouble to relay this game for us all... but yeah sure right he's so totally lazy MeiGuoTang
MeiGuoTang: I guess sarcasism is a thing in the past?
jillgoodgal: Mei has a very acerbic sense of humor
MeiGuoTang: "no moves have been played, must be the relayers fault"
S_Alexander: most watched game atm is hane naoki vs iyama yuta
S_Alexander: second one is toramaru's
bgauch: where are you watching S_Alexander?
S_Alexander: wbaduk
S_Alexander: i don't use igs
jillgoodgal: hopefully they pick up again at the bottom of the hour
S_Alexander: should continue at 30 min, almost now
MeiGuoTang: Aqua
](;W[lg]C[
-- chat --
AquaBot: yes K16 stones area dead
](;B[om]C[
-- chat --
gemm: m13!
gemm: alpha was (of course) right all along
gemm: yes sorry
gemm: unconfortable to permit p7 though
](;W[on];B[nn];W[nl];B[ml];W[ol]C[
-- chat --
YellowFresko: Win rate now?
MeiGuoTang: B+
MeiGuoTang: I imagine its still around 65-68%
trung12ly: He is over 70 i believe.
];B[jp]C[
-- chat --
AquaBot: I can give winrate
AquaBot: W 65
BHydden: wait... W65?
BHydden: not B 65?
S_Alexander: black made mistake?
];W[jo]C[
-- chat --
MeiGuoTang: Guess so
];B[io](;W[in]C[
-- chat --
jillgoodgal: lower right black stones under some pressure now
jillgoodgal: move is 138?
];B[hp](;W[ko]C[
-- chat --
S_Alexander: ok, my script broke, will have to relay manually
jillgoodgal: no 136 I see it now
jillgoodgal: and now 138
AquaBot: my mistake earlier it was 65 B
AquaBot: now it is 73 B
jillgoodgal: still move 138?
SlowLarry: no numbers on the variation stones
MeiGuoTang: it is annoying but meh
MeiGuoTang: I wanna see twitch vs pros game xD
Wong Kar-Wai: What's bigger? K15 or something like D8?
Wong Kar-Wai: Or is something like O3 necessary?
jillgoodgal: I think black only needs to worry about connecting underneath right now if white can get O6
jillgoodgal: yes
jillgoodgal: yes I think black wants to come down from that central group
gemm: cutting n6 would be too big
jillgoodgal: lots of ways to head for a connection though
Wong Kar-Wai: Oh what? They haven't come back from lunch?
S_Alexander: no new moves on wbaduk
jillgoodgal: it does seem only two moves
Wong Kar-Wai: Maybe they called it quits because the AC broke lol
jillgoodgal: I think maybe E7 is intended to be followed up with D9 or B9?
Wong Kar-Wai: I don't see how. White's response would be E9.
jillgoodgal: then black plays B9
Wong Kar-Wai: And then?
Wong Kar-Wai: White just descends.
paytonbigsby: Hello all
jillgoodgal: did Michael resign?
S_Alexander: still no moves
S_Alexander: this is weird
benjito: What is ogs relay
jillgoodgal: move 138
benjito: Move 139
newbealltime: Maybe F6 or K9?
jillgoodgal: someone said a bot likes E7 for black
S_Alexander: well, they have lots of time so might as well use it
Wong Kar-Wai: What are the match settings?
S_Alexander: 3 hours each
Wong Kar-Wai: Alllllrighty then.
jillgoodgal: not counting that lunch break
Wong Kar-Wai: Please Lord Cho Chikun, make them go.
S_Alexander: you have no idea how slow cho plays in main time lol
Wong Kar-Wai: lol yeah
Wong Kar-Wai: I've seen him get frustrated by the clock
S_Alexander: meijin league has 5 hours each so we're getting off easy
Wong Kar-Wai: Well. This is disheartening.
S_Alexander: i wonder if anything happened
S_Alexander: otake is old
jillgoodgal: just planning the most awesome move I am sure
](;B[mk];W[jm];B[oj];W[pj];B[nj];W[pm];B[pi];W[ni];B[mi];W[nh];B[ln];W[lj];B[mj]C[
-- chat --
jillgoodgal: aha here we go
];W[kf];B[il];W[hm];B[fl]C[
-- chat --
jillgoodgal: extend at any cost
];W[gk];B[hj];W[hk];B[fk];W[ik];B[bq];W[br]C[
-- chat --
S_Alexander: finally
AquaBot: B 64 winrate
AquaBot: C9 and Q9 biggest moves
S_Alexander: still close then?
];B[ck];W[dk];B[cj];W[dj];B[bi];W[bl](;B[kk];W[kl];B[kj];W[ki];B[lk];W[pb];B[rb];W[gp];B[em];W[cl];B[bk];W[ak];B[bj];W[dm];B[qr];W[rr];B[pr];W[gq];B[hr];W[mq];B[mr];W[rs];B[ek];W[dl];B[jl]C[
-- chat --
jillgoodgal: has to connect K10 now?
jillgoodgal: or K9 works?
mt3000: hasnt to connect, he is alive
S_Alexander: endgame is so boring
S_Alexander: how is the win%¿
jillgoodgal: don't see how he is going to get a favorable result in that upper left corner
neverland: I'm getting 56.5% for black
jillgoodgal: A13 seems a weak point but I just can't see that working
S_Alexander: i'm thinking
S_Alexander: maybe it's wbaduk relayer who's fallen asleep
];W[nb](;B[mb];W[fm];B[bp]C[
-- chat --
jillgoodgal: therethey go
];W[bo];B[en];W[el];B[fo];W[gl];B[fp];W[ho];B[fn];W[gm];B[ei];W[kn];B[jk];W[ip];B[kp];W[lo];B[nq];W[hc];B[hb];W[si];B[aa];W[ac];B[ca]C[
-- chat --
jillgoodgal: corner battle
Kountch: seki
go for the gun: IT BEGINS
omeara: how is it possible that this is only beginning now? amazing
];W[mp];B[af]C[
-- chat --
paytonbigsby: No one likes 1000 year ko
];W[dn]C[
-- chat --
Kountch: Here it looks like more than 1000 (there is an extra liberty). Maybe 1500 years.
AquaBot: here's a graph of the winrate for this game https://i.imgur.com/RKYfM1o.jpg
jillgoodgal: point on one side or the other of that F3 line at least
AquaBot: This is using the strongest Leela Zero net available.
neverland: Can anyone watch on wbaduk?
raphael87: 4 step KO
raphael87: its very tough
neverland: I was trying to find the game, but I'm too dumb to navigate the website
BHydden: wow 97% for white :O
jillgoodgal: surely black won't give him both points around that line off the flower?
MeiGuoTang: half point game
AquaBot: Here's the winrate graph using leela Master https://i.imgur.com/kD7qH0I.jpg
omeara: Redmond bringing that immaculate endgame
omeara: knock on wood
Kountch: Is Redmond strong at endgame?
MeiGuoTang: I'd love see Redmond make it into S league for honinbo
MeiGuoTang: or w/e this league is
jillgoodgal: he was awesome last game
jillgoodgal: in endgame
omeara: From his AlphaGo review series he gives the impression that he is a devoted student of endgame, even more so than most pros
omeara: though im sure those at his level are all astounding at it
omeara: things like “Ive read out the last 75 moves from here, and it does indeed seem like Black maintains a 1.5-stone lead”
MeiGuoTang: Jowa as a conception, become a top professional you must master 1 one the phases of the game.
jillgoodgal: wouldn't F5 first be better for black?
MeiGuoTang: One of his translated phrases
MeiGuoTang: White will lose
MeiGuoTang: unless white can find half a point more than black
jillgoodgal: well black has to pick his points now
MeiGuoTang: there's a lot of 2 points left
MeiGuoTang: that's it
trung12ly: Oh, Otake Hideo, author of my fav book:)
jillgoodgal: Opening Theory Made Easy?
trung12ly: Yes
jillgoodgal: the extend at any cost principle did show in his game here
jillgoodgal: those two potential captures seem big in such a close game since white won't be able to retake
](;B[ds];W[ar];B[lh];W[ld];B[nc];W[jj];B[bh];W[sh];B[sg];W[ok];B[aj];W[al];B[qs]C[
-- chat --
S_Alexander: effing finally
];W[li]C[
-- chat --
pianoman: aah I issed most of it. do we know who is winning?
];B[ad];W[ph];B[na];W[ob];B[gj]C[
-- chat --
jillgoodgal: white saved both but could lose more points on the 1 line
];W[qa];B[ra];W[gc];B[nf];W[ng];B[gb];W[id];B[he];W[oa];B[ma];W[dh];B[ej];W[eh];B[fh];W[kh];B[pp];W[le];B[mf];W[je];B[gn];W[cs];B[db];W[ba];B[ll];W[km]C[
-- chat --
paytonbigsby: Much appreciated
];B[aa];W[es];B[fr];W[ba]C[
-- chat --
PetAccount: White wins by resign.
PetAccount: Thanks for watching.
omeara: Yes thank you
jillgoodgal: so must be those two stones he saved
Hrewsahgs: Thanks for the relay
trung12ly: Lol, i was thinking he tried to draw 'wtf'
lemonjin1997: thank you
jillgoodgal: so I see one point to be decided by E5 plus the three ko, plus the point at N6 that black can't really get
]N[W+R])(;B[lm];W[km];B[ds];W[ar];B[nc];W[lh];B[li];W[ph];B[sh];W[ld];B[je];W[id];B[he];W[mf];B[le];W[ke];B[dh];W[gj];B[bh];W[gi];B[gh];W[ok];B[qs];W[mo];B[nf];W[ng];B[mn];W[aj];B[cs];W[bs];B[na];W[ob];B[ai];W[al];B[gn];W[es];B[ds];W[eo];B[gc];W[qa];B[ra];W[hn];B[fr];W[pa];B[jj];W[oa];B[ej];W[fj];B[md];W[nd];B[od];W[db];B[op];W[pp]C[
-- chat --
AquaBot: Endgame. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 J6 H4 L5 N9 K7 P10 Q10 O10 Q7 Q11 O11 N11 O12 M6 M10 N10 L14 J8 H7 F8 G9 H10 H9 F9 J9 B3 B2 C9 D9 C10 D10 B11 B8 L9 L8 L10 L11 M9 Q18 S18 G4 E7 C8 B9 A9 B10 D7 R2 S2 Q2 G3 H2 N3 N2 S1 E9 D8 K8 O18 N18 F7 B4 B5 E6 E8 F5 G8 F4 H5 F6 G7 E11 L6 K9 J4 L4 M5 O3 H17 H18 T11 A19 A17 C19 N4 A14 D6 M7 L7 D1 A2 O17 M12 M11 Q12 T12 M16 K15 J16 H15 N14 M15 L15 D12 G10 B12 G11 G12 P9 R1 N5 O14 O13 N6 A10 C1 B1 O19 P18 A11 A8 G6 E1 D1 E5 G17 R19 S19 H6 F2 Q19 K10 P19 E10 F10 N16 O16 P16 D18 P4 Q4
]))(;B[ld];W[gj];B[ei];W[kn];B[lh];W[ph];B[nf];W[ng];B[jk]C[
-- chat --
AquaBot: 23. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 J6 H4 L5 N9 K7 P10 Q10 O10 Q7 Q11 O11 N11 O12 M6 M10 N10 L14 J8 H7 F8 G9 H10 H9 F9 J9 B3 B2 C9 D9 C10 D10 B11 B8 L9 L8 L10 L11 M9 Q18 S18 G4 E7 C8 B9 A9 B10 D7 R2 S2 Q2 G3 H2 N3 N2 S1 E9 D8 K8 O18 M16 G10 E11 L6 M12 Q12 O14 O13 K9
]))(;B[em];W[cl];B[bj];W[dm];B[gj]C[
-- chat --
AquaBot: this is why. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 J6 H4 L5 N9 K7 P10 Q10 O10 Q7 Q11 O11 N11 O12 M6 M10 N10 L14 J8 H7 F8 G9 H10 H9 F9 J9 B3 B2 C9 D9 C10 D10 B11 B8 E7 C8 B10 D7 G10
]))(;B[ln];W[kf];B[fo];W[jm];B[ck];W[dk]C[
-- chat --
AquaBot: this is why. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 J6 H4 L5 M6 L14 F5 K7 C9 D9
])(;B[em]C[
-- chat --
Wong Kar-Wai: You mean N6?
];W[ek]C[
-- chat --
Wong Kar-Wai: Turns out E7.
Wong Kar-Wai: Feels like a lonely move.
Wong Kar-Wai: Too easily disconnected, no?
Wong Kar-Wai: Or perhaps just misdirection to bring the whole group down.
];B[gk];W[fl];B[gl];W[gm]C[
-- chat --
Wong Kar-Wai: Yeah.
]))(;W[ho]C[
-- chat --
MeiGuoTang: ?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 J6 H4 H5
]))(;W[hp];B[jn]C[
-- chat --
MeiGuoTang: ?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 P6 O6 O8 N8 P8 K4 K5 J5 H4 K6
]))(;W[nl];B[on];W[kf];B[pm];W[rm];B[ml];W[mk];B[lk];W[nk];B[mn];W[jo];B[fk];W[kp];B[lj];W[kl];B[ll];W[dk];B[nh];W[mf];B[ok];W[pk];B[ol];W[ni];B[rl];W[qm];B[ql];W[pi]C[
-- chat --
AquaBot: 22. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 O8 P6 L14 Q7 S7 N8 N9 M9 O9 N6 K5 F9 L4 M10 L8 M8 D9 O12 N14 P9 Q9 P8 O11 S8 R7 R8 Q11
];B[oj];W[pj];B[nj];W[mi];B[mj];W[pl]C[
-- chat --
MeiGuoTang: Not ideal for black as he loses more than 8 points here. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 P7 O8 P6 L14 Q7 S7 N8 N9 M9 O9 N6 K5 F9 L4 M10 L8 M8 D9 O12 N14 P9 Q9 P8 O11 S8 R7 R8 Q11 P10 Q10 O10 N11 N10 Q8
]))(;B[ml];W[mn];B[nn];W[mo];B[pm];W[nl];B[nk];W[ol];B[km];W[kp];B[mq];W[il];B[im];W[jl];B[jm];W[ho]C[
-- chat --
AquaBot: 21. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 N8 N6 O6 N5 Q7 O8 O9 P8 L7 L4 N3 J8 J7 K8 K7 H5
])(;B[pj]C[
-- chat --
MeiGuoTang: I imagine?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 Q10
])(;B[je]C[
-- chat --
AquaBot: N8
](;W[ke];B[kf];W[le];B[mf];W[md];B[nd]C[
-- chat --
MeiGuoTang: Whites dead. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 K15 L15 L14 M15 N14 N16 O16
])(;W[kf];B[ke];W[ml];B[lm];W[ll];B[om];W[on];B[nn];W[nl];B[pj];W[pi];B[qn];W[pm];B[qk];W[rk];B[rl];W[qm];B[ql];W[rm];B[sk];W[pk];B[si];W[oj]C[
-- chat --
AquaBot: Whites not dead. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 M13 K15 L14 L15 N8 M7 M8 P7 P6 O6 O8 Q10 Q11 R6 Q7 R9 S9 S8 R7 R8 S7 T9 Q9 T11 P10
MeiGuoTang: Aqua I was anwsering the k15 question a 10k had
AquaBot: ah okay
bgauch: oh yes, we were talking about the 5 stones in the top
MeiGuoTang: k16 stones are most certianly dead stones
])(;W[ml]C[
-- chat --
AquaBot: I will shwo you why M13 is important
])))(;W[pl];B[nl];W[nn];B[lg];W[li];B[mn];W[ml];B[mi];W[ll];B[nj];W[oj];B[ok];W[pk];B[pi];W[pj];B[ng];W[ni]C[
-- chat --
AquaBot: Q8 move variation B87% winrate. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 Q8 O8 O6 M13 M11 N6 N8 N11 M8 O10 P10 P9 Q9 Q11 Q10 O13 O11
])(;W[ml](;B[lm];W[lg]C[
-- chat --
MeiGuoTang: what about this for white?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N8 M7 M13
])(;B[lg]C[
-- chat --
AquaBot: Both M16 and K15 response are very bad to M13
](;W[lh];B[li];W[kh];B[ng];W[nh];B[mf];W[mi];B[mj];W[mg];B[bq];W[br];B[mh];W[ni];B[pj];W[pi];B[ll];W[lm];B[mn];W[lk];B[om];W[kl];B[qm];W[rm];B[nl];W[nk];B[rl];W[pk];B[pl]C[
-- chat --
AquaBot: This is why M13 is important. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N8 M13 M12 M11 L12 O13 O12 N14 N11 N10 N13 B3 B2 N12 O11 Q10 Q11 M8 M7 N6 M9 P7 L8 R7 S7 O8 O9 S8 Q9 Q8
MeiGuoTang: what?
bgauch: in this variation, white loses most of the center-right area he tried to build up, but gets more influence in the area around J7. Still, black has so much of the sides, with white almost all in the center, I would be nervous.
])(;W[ld]C[
-- chat --
AquaBot: M16 will end game
AquaBot: B win
];B[je];W[md];B[nd];W[nc];B[mb];W[mf];B[ke];W[lh]C[
-- chat --
MeiGuoTang: Aqua. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N8 M13 M16 K15 N16 O16 O17 N18 N14 L15 M12
])(;W[mf];B[lh];W[ld];B[nc]C[
-- chat --
MeiGuoTang: To sacrifice but it actaully doesnt work. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N8 M13 N14 M12 M16 O17
])(;W[je];B[li];W[mj];B[kk];W[jk];B[kj];W[jj];B[pj];W[pi];B[pk];W[ql];B[om];W[nl];B[on];W[pl]C[
-- chat --
AquaBot: You're 10 kyu, you can make as many mistakes as you want compared to pro game
AquaBot: W should play M13 right now
])))(;W[mf]C[
-- chat --
AquaBot: Pretty good move. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14
](;B[lg];W[lh]C[
-- chat --
gemm: hmm maybe even sente Aqua?
])(;B[je];W[le];B[ke];W[ld];B[lg];W[lh];B[li];W[kh];B[kf];W[md];B[nd];W[nc];B[mb](;W[nf]C[
-- chat --
AquaBot: Pretty good move. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 O14
gemm: k12 die though
gemm: yes with the atari he's ok
];B[mi]C[
-- chat --
AquaBot: K12 not dead
];W[jk];B[kj];W[jj]C[
-- chat --
AquaBot: K12 group not dead here. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 O14 N11 K9 L10 K10
bgauch: well, top left is already looking like a ko, right? So I guess he needs ko threats for later, yes.
])(;W[mi]C[
-- chat --
MeiGuoTang: Why not here?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 N11
];B[mj];W[nh];B[hc]C[
-- chat --
AquaBot: this is why. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 N11 N10 O12 H17
];W[mn]C[
-- chat --
MeiGuoTang: I mean capturing the center would be just as good?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 N11 N10 O12 H17 N6
MeiGuoTang: I mean capturing the center would be just as good?its right under the shapes in the analyze bar on the page. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 K15 M15 L15 M16 M13 M12 M11 L12 L14 N16 O16 O17 N18 N11 N10 O12 H17 N6
MeiGuoTang: its right under the shapes in the analyze bar on the page
]))(;B[kj];W[jj];B[mk];W[ok]C[
-- chat --
bgauch: 10 kyu variation with "pretty good move" :). From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N14 L10 K10 N9 P9
]))(;W[mn];B[nn];W[ml];B[nl];W[lm];B[om];W[ol];B[mm];W[nk];B[ll];W[nm];B[mk];W[lk];B[lj];W[mj];B[kk];W[kl]C[
-- chat --
MeiGuoTang: White resigns. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 N7 N6 O6 N8 O8 M7 P7 P8 N7 O9 M8 O7 N9 M9 M10 N10 L9 L8
bgauch: hahaha Mei, that variation looks like one of my first games of Go. :)
MeiGuoTang: is it on IGS?
]))(;B[lg];W[lh](;B[je];W[mf];B[kf]C[
-- chat --
AquaBot: 19. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 M13 M12 K15 N14 L14
ckersch: N8 seems so passive
])(;B[li];W[kh];B[ng];W[mi];B[mf];W[nh];B[mj];W[ni];B[pj];W[pi];B[mm];W[mn];B[om]C[
-- chat --
AquaBot: 20. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 N12 N13 M13 M12 M11 L12 O13 N11 N14 O12 N10 O11 Q10 Q11 N7 N6 P7
AquaBot: nah, it's jus t that otake didn't play best move.
])))(;B[je]C[
-- chat --
AquaBot: 18. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 O15 P15 R11 S10 K15
]))(;B[lh]C[
-- chat --
AquaBot: 14. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 M12
AquaBot: 16. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 M12
AquaBot: 1-4 all playable for W
AquaBot: B 80% winrate now
])(;B[qi];W[rk];B[lh];W[li];B[je];W[mg];B[lg];W[mh];B[mm];W[mn];B[om];W[on];B[nn];W[nl];B[lj];W[mi];B[pj];W[mo];B[qk];W[rj];B[ql];W[qm]C[
-- chat --
AquaBot: 17. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 R10 C3 C4 D3 E3 E2 F2 F3 E4 G2 D2 F1 C2 J15 K14 H14 G5 M14 H3 J3 J4 J2 R17 S17 R13 S13 Q16 R18 Q17 S15 S11 S12 P11 R11 S9 M12 M11 K15 N13 M13 N12 N7 N6 P7 P6 O6 O8 M10 N11 Q10 N5 R9 S10 R8 R7
jillgoodgal: the left side of the 10/11 line is important
jillgoodgal: up 3%!
jillgoodgal: Redmond must have done something key
jillgoodgal: a few points can be decided by Q8
jillgoodgal: yes too small right now
jillgoodgal: not M13
]))(;W[ie]C[
-- chat --
AquaBot: 12. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 M3 J15
AquaBot: J15 is vital for W to play
]))(;B[kp];W[ln];B[mn];W[ko];B[lq]C[
-- chat --
AquaBot: 11. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 O5 M4 L4 M6 N6 L5 M3
]))(;B[mn]C[
-- chat --
AquaBot: 9. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 N6
AquaBot: m4 WILL be a big mistake for W here
bgauch: It looks to me like the top-right corner is either ko or seki. If black plays locally first (at A16), he can make it seki. If white plays in the top right first, it is a ko. If anyone in the chat is better at life and death (i.e., above 10k) correct me if I am wrong. :)
bgauch: sorry, I meant top-left
];W[lp];B[kp];W[ln];B[on];W[lq];B[mm];W[ko];B[mo];W[lo];B[pm];W[qn]C[
-- chat --
AquaBot: 10. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P3 R3 L3 O7 N6 M4 L4 M6 P6 M3 N7 L5 N5 M5 Q7 R6
]))(;B[on];W[pm];B[om];W[pl];B[ol];W[pp];B[op];W[oq];B[nq];W[or];B[nr];W[pr];B[qq];W[rq];B[mp];W[mg]C[
-- chat --
AquaBot: 7. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q6 P5 R4 P6 Q7 P7 Q8 P8 Q4 P4 P3 O3 P2 O2 Q2 R3 S3 N4 N13
liminal: White lost the influence I thought he was fighting for on the bottom left.
liminal: There could have been, if he traded the corner to black.
liminal: no?
liminal: It's about 20 points for white.
liminal: Would you agree?
liminal: How many points would you currently give to white for that corner?
liminal: q3 and r3 are both captured, with no way around that I can see. What are you seeing?
liminal: I'm talking about the bottom right corner.
liminal: I'm suggesting it's worth about 20 points for white.
liminal: MeiGuoTang suggested it was worth 9.
liminal: I'm trying to understand that count.
]))(;W[pp];B[op];W[qp];B[oq];W[oo];B[pn];W[qq];B[nq];W[on];B[qn];W[om];B[ro];W[rp];B[rn];W[rr];B[mp];W[pk];B[qk];W[qj];B[rk];W[pj];B[ri];W[ie]C[
-- chat --
AquaBot: 8. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 Q14 P14 R14 P13 R12 C11 F11 C7 H12 J12 J11 K11 J13 K12 H13 K13 N17 R5 N15 J10 H11 O4 Q5 Q4 P4 R4 P3 P5 Q6 R3 O3 P6 R6 P7 S5 S4 S6 S2 N4 Q9 R9 R10 S9 Q10 S11 J15
]))(;B[mc];W[ld];B[pf];W[of];B[qf];W[og];B[nd]C[
-- chat --
AquaBot: Which variation would you like to see?. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 L16 K18 M17 M18 K17 L18 Q15 R15 Q13 N17 M16 Q14 P14 R14 P13 O16
]))(;W[pe]C[
-- chat --
AquaBot: 6. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 F19 E19 J18 Q15
AquaBot: W winrate 43 at move 35 however///
]))(;B[ie];W[je];B[ff];W[fe];B[hf];W[gg];B[ig];W[he];B[jf];W[id];B[gh];W[if]C[
-- chat --
AquaBot: 3. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 J15 K15 F14 F15 H14 G13 J13 H15 K14 J16 G12 J14
AquaBot: 5. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 B15 B14 A18 A15 B16 J15 K15 F14 F15 H14 G13 J13 H15 K14 J16 G12 J14
]))(;W[ab];B[fe];W[ff];B[ie];W[gg];B[jf];W[be];B[bf];W[gi]C[
-- chat --
AquaBot: 4. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 D17 F18 B17 B18 A18 F15 F14 J15 G13 K14 B15 B14 G11
IAMTRADER: what is white winrate on jumping G11 ?
IAMTRADER: yep
AquaBot: 40
IAMTRADER: thanks, so giving black top territory is not a good idea
]))(;W[bb]C[
-- chat --
IAMTRADER: sure
];B[bc];W[bd];B[ba];W[ab];B[ac];W[fb];B[gb];W[hc];B[gc];W[dc];B[hf]C[
-- chat --
IAMTRADER: 2. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 C18 B18 B17 B16 B19 A18 A17 F18 G18 H17 G17 D17 H14
]))(;B[ie];W[je];B[fe];W[ff];B[hf];W[gg];B[ig]C[
-- chat --
IAMTRADER: black can cut i think. From move 0: R16 D4 Q3 D16 F17 P17 C14 D14 D13 E14 C15 C16 G16 E18 L17 D11 E13 G14 F13 G15 C12 J17 H16 K16 D15 E15 E16 E17 F16 J14 J15 K15 F15 F14 H14 G13 J13
]))

View File

@ -0,0 +1,184 @@
(;GM[1]FF[4]SZ[19]AP[SmartGo:3.1.4]CA[utf-8]
GN[shuwa_genan]
PW[Inoue(Genan)Inseki]WR[8P]
PB[Honinbo Shuwa]BR[7P]
DT[1842-5-16-18]
RE[B+6]MU[0]
C[Shuwa became the leading player in the mid-19th century after the previous Honinbo Jowa retired.
Genan was a strong player of that time who had previously tried to take Honinbo Jowa down from his post as Meijin (leader of the Go community and recognized to be the strongest player).
Genan's playing style was often brilliant and exciting, but he failed to beat the Honinbos Jowa, Shuwa, and Shusaku in the most important games of his career. This game from 1842 is the second of a three game challenge match in which Genan hoped to show his ability to beat Shuwa with White (no komi), but lost all three games, after which the match was discontinued. ]
MU[0];B[qd];W[oc];B[ec];W[pf]LB[ec:A]
C[The Taisha at White 4 was a move that Genan liked to play. The joseki is well known to be complicated, and Genan had some special variations of his own. Indeed, he caught Shusaku once with a new variation to get a distinct advantage, but could not take it to a win after Shusaku played the ear-reddening move in one of the most famous games of Go history.
In this game Shuwa was ready for the Taisha joseki, as we see he has played in the UL corner at A, which will be well placed when the usual fighting joseki happens in the UR corner (var for White 6). ]
;B[od]
(;W[nd]
C[Genan did not play the fighting variation here (see var). ];B[oe];W[pc]
(;B[qe]TR[of]
C[Black's bamboo joint was the standard move when the ladder favors White (see var). Playing Black 9 at the marked point is a modern variation of this joseki that was not yet played in the 19th century.]
;W[qc];B[qi];W[de];B[cd];W[ce];B[bd];W[mf]LB[qg:A]
C[White 16 threatens the invasion at A, and prepares to build a moyo on the upper side. ]
;B[of];W[gc]
(;B[fc]
C[Black 19 was not an expected move (see var). Honinbo Shuwa seemed to have a strategy of simplifying the game at the cost of giving up some of his original advantage (Black with no komi), and quite often his games with Black would seem to be about even after the opening. However, he was extremely good at converting the smallest advantage into a win in the middle to endgame. ]
;W[gd];B[ee];W[do]LB[cp:A]
C[With Black's solid group in the upper right, I agree that the LL corner seems to be bigger than the LR corner. I would have played a lower point, maybe at A. ]
(;B[cj]
C[Human pros did not like Black 23, and it was completely off the map when Zen analyzed the game. Zen suggested an invasion to the upper side (see var). ]
;W[di]LB[dq:A][qq:B]
C[I imagine AlphaGo would give White a thumbs-up for this shoulder hit, if only it could. While corner plays such as A or B are surely candidates in this board position, it seems clear that White 24 is the best move for the left side.]
;B[ci];W[dh];B[cm];W[fe]TR[ce]LB[qp:A]
C[White is willing to sac the two marked stones, and is trying to link up onthe outside and expand the moyo on the upper side.]
;B[ef];W[ff];B[eg]
(;W[fg]
C[With 32 Genan seemed to get a bit carried away, and I would call this an overplay. There was an easier way to connect on the outside (see var). ]
;B[eh];W[fh];B[ei];W[dj];B[dk];W[ej];B[fi]LB[fg:A]
C[The fight now favors Black, and I will blame White's overplay at A for this. ]
;W[dl]LB[fg:A]
C[My guess is that this clamp is the move that Genan had in mind when he played the forceful sequence starting with A. ]
(;B[ek]
C[Connecting beneath would have been weak (see var), and naturally Black pushes through. ]
;W[fj];B[gi];W[dd];B[dc];W[ck]
(;B[el]
C[With 47 Black could not cut on the third line (see var). ];W[bk];B[ch]
(;W[cg]TR[dj][bk]LB[dg:A]
C[Covering here is a local sacrifice that gives White time to save the two marked groups. If White had pushed through at A, Black could have captured one or the other (see var). ]
;B[dg];W[fl]LB[gj:A]
C[White 52 was the good shape move, the alternative being to simply push out at A. ]
(;B[fk]LB[em:A]
C[If Black had simply extended at A, that would give White a chance to win the semeai on the left (see var). ]
;W[gj];B[bg];W[cf];B[ed]LB[ed:A][bf:B][hi:C]
C[With A, Black wins the semeai while eliminating any possibility for White to play a forcing move from the center. For example, if Black had played at B, White would have been able to force with C. ]
;W[dm]TR[bk]
C[White saves the marked stones.];B[em];W[dn];B[gl];W[hj];B[hi];W[ij];B[il]
(;W[jf]
C[White's weak group in the center combined with White's loosely surrounded moyo on top gives White a thin shape overall. On the left, a cut on the second line might seem to have potential, but Black can win the semeai (see var). ]
;B[jh]LB[jg:A]
C[This knight's move from a living group is slack, but there is some merit in keeping the game simple when Black is already ahead. Pros analyzing the game have hesitated to call this move a mistake because we know that Shuwa had an almost perfect record for using such moves to maintain a small lead. I would have played at A without thinking twice, and Zen tells me that my move is better. ]
;W[kj];B[gn]LB[gn:A]
C[While Black A is indeed a vital point in Black's shape, agian I get the feeling that Shuwa is saying that he is confident of a win. ]
;W[oi]LB[ph:A][qh:B]
C[With this capping move Genan is trying to reinforce the center while putting pressure on Black's right side. Zen suggests White A, Black B, and the playing away to one of the corners on the lower side. ]
;B[hg]LB[ie:A]
C[Another tight move, but this is big with the extra value of Black A next. ]
;W[qj];B[rj]
(;W[qh]
C[Throughout the years, this move has been critisized as an overplay, and the computer analysis seems to agree (see var). ]
;B[pj];W[pi];B[qk];W[ri]
C[White has challenged Black to a ko, when it seems that Black should be able to manufacture some ko threats on the upper side to have an advantage. The way Shuwa does this is instructive. ]
;B[ie]LB[rh:A]
C[If Black had immediately cut at A to start a huge ko, chances are that White would have ignored any ko threats towards the upper side. Therefore, Shuwa lets the right side wait, attacking the upper side first. ]
;W[if];B[hf]
(;W[jd]
C[Cutting was dangerous (see var). ];B[hd];W[hc];B[me]TR[od][qd]LB[rh:A]
C[Black is hoping to make some forceful ko threats on the upper side, also looking for a way to strengthen the marked group. The ko at A is still waiting to happen. ]
(;W[lf]
C[White pulls back to keep the pressure on Black's group on the right. The variation shows how it would have been easier for Black if White had simply pushed through. ]
;B[ne];W[md];B[le];W[kd];B[kf]
C[By cutting White off, Black weakens White's shape in the center and increases the number of ko threats for Black on the upper side. ]
;W[kg];B[ke];W[je];B[jg];W[kh];B[rc];W[rb];B[rh];W[qj];B[ld];W[lc];B[qi]
(;W[bh]
C[From the start, White was counting on these ko threats on the left, but there will not be enough of them now that Black has plenty on the upper side. Maybe White can try to kill Black in the upper right without a ko? No, it would end badly (see var). ]
;B[bi];W[qj];B[mc];W[nc];B[qi];W[bf];B[ah];W[qj];B[kc];W[mb];B[qi];W[af]
;B[cl];W[bm];B[ki]LB[ag:A]
C[This is a point where Black could have won the semeai on the left by playing at A, but took this chance to ignore White's ko threat. Not to resolve the ko just yet, because Black still has plenty of ko threats, and attacks in the center first. ]
;W[qj];B[lb];W[mc];B[qi];W[ag]
C[The left side is now a step ko that favors White locally, but it will take two more moves for White to completely resolve this ko. Black's plan will be to obtain an advantage on the rest of the board while White must spend two moves on the left. ]
;B[lj];W[qj];B[ic];W[jc];B[qi];W[li];B[ji];W[qj];B[ib];W[id];B[qi];W[mj]
;B[lk];W[qj];B[he];W[jb];B[qi];W[mh];B[si]TR[mh]
C[Finally the ko ends. The marked White group is not alive yet. ];W[oj];B[ok]
;W[mk];B[pq];W[jj]LB[jj:A]
C[White's connection at A has always been a mystery to me, to the extent that I have asked if it was a mistake in the game record. Other commentators have similarly failed to find a reason for this move, and I am relieved to be able to say that the computer does not show it as a candidate. ]
;B[ll];W[ol];B[eq];W[cq];B[mp];W[bh]LB[bh:A][be:B][gq:C]
C[Not to forget that White had this ko to deal with at A. Otherwise, Black could have made a one-step approach ko by playing at B. Could White have invaded at C first? I would like to, and after erasing the lower side White would ahve an advantage in territory, but the ko would be very dangerous. ]
;B[gp];W[nk]LB[nk:A]
C[White A is a point where Black could have played to attack White's eye space in the center. While it was not immediately necessary, it was a large move that takes away Black's threats and reduces Black's potential on the right side. ]
;B[mn]LB[lo:A]
C[Could it be that Shuwa was a bit greedy here? Black is winning, and I would have been happy to play the more conservative A, but maybe that's just because I've seen this game and I know what happens next. ]
;W[gq];B[fq];W[hp];B[hq];W[mq];B[nq];W[lp]
(;B[gr]
C[What a patient and quiet move! I would want to cut again, of course (see var). ]
;W[nr];B[np];W[kq]LB[mr:A][lr:B][lm:C]
C[If Black cuts at A, White offers to play a ko with B. White doesn't have enough ko threats elsewhere, but could use local ko threats such as C for this ko. ]
;B[or]
C[Black declines to play the ko. ];W[lr];B[jr];W[ln];B[lm];W[jq];B[pk];W[nm]
;B[lo];W[ko];B[kn];W[jo];B[ns];W[mr];B[pm]LB[pm:A][gq:B]
C[Shuwa offers no resistance, allowing White to live without even a ko. However, when Black plays at A, it suddenly appears that Black has not lost anything. Black's territory just switched from the lower side to the right. Failing to find anything better for either side, I will say that the sequence from White's invasion at B ended in an even result. ]
;W[on];B[pn];W[bc]LB[cc:A][ai:B][ad:C]
C[By exchanging this move for Black A before finishing the ko with White B, White created a big move at C, gaining some points in the corner. ]
(;B[cc];W[ai]LB[bc:A][cc:B][ad:C]
C[The exchange of A for B was a cleverly timed exchange that set up the connection at C and made the game very close. ]
;B[ir];W[kr];B[cr];W[ad];B[bb];W[br];B[dr];W[ab]
C[Yet another ko starts. Since the life of Black's group is at stake, and White's potential local loss is small, Black will seek a peaceful ending here. ]
;B[ac];W[oo];B[pl];W[bc];B[ca]LB[ac:A][gb:B][aa:C]
C[If White connects at A, Black can live with B. Otherwise, Black can capture two stones in the corner with C. ]
;W[qg];B[og];W[ph];B[rg];W[fb];B[ac];W[op];B[oq];W[bc];B[eb];W[nn];B[gb]
;W[hb];B[fa];W[rd];B[re];W[sc];B[cp];W[bp];B[dp];W[co];B[ip];W[jp];B[in]
;W[fo];B[fn];W[eo];B[ho];W[ac];B[kk];W[qf];B[rf];W[jn];B[jm];W[ps]LB[cs:A]
C[It has been suggested that Genan misread the tsumego in the lower right. However, if one assumes he just played the biggest move at A (maybe with some optional one point forcing moves first), the game would have ended with Shuwa winning by one point. Having already had the experience of losing to Shuwa by the smallest margin, maybe Genan chose to avoid that and take the more exciting ending. ]
;B[os];W[qo];B[po];W[qq];B[pp];W[qr];B[rn];W[ro];B[so]
(;W[cs]
C[Having lost five points in the LR corner, Genan will lose by six points now. He decided not to finish the tsumego, so I will do it for him in a var. ]
;B[ds];W[bs];B[se];W[jk];B[jl];W[gk];B[hl];W[en];B[fm];W[no];B[mo];W[js]
;B[is];W[ks];B[sd];W[rc];B[ms];W[ba];B[aa];W[sp];B[qp];W[ba];B[cb];W[ae]
;B[aa]
C[The result is recorded as Black winning by six points, when White can win the final ko to get a 5 point difference. Black A (253) should have been played at B to get the correct result, and I think this could be a mistake in the game record. ]
LB[hl:A][hk:B])
(;W[sp];B[qp];W[rp];B[sr];W[rr];B[sq];W[ss];B[qs];W[pr];B[rs];W[ss];B[rs]
;W[rq];B[sn];W[ss];B[sr]LB[ss:A][sq:B]
C[Some extra moves have been added to simplify this for my explanation. This is called the bent four in the corner, and White has no viable local moves, but it is not a seki. Afterfilling all outside liberties, Black can play A, the B, and after White takes the four stones it will be a ko in which Black takes first. Since Black can choose the timing of this ko, this shape is ruled dead in Japan. ]
))
(;B[ad];W[cc];B[bb];W[ac];B[cb];W[dr];B[bg];W[aj];B[bj];W[ak];B[bl];W[cn]
;B[df];W[al]TR[bf]LB[bc:A][ad:B][cc:C]
C[If Black answers White A at B, the cut at C gives White's marked group an extra liberty and makes the threatened step ko more distant. As this var shows, White can even win the semeai without a ko. ]
))
(;B[lq];W[mr];B[lr];W[np];B[mo];W[oq];B[nr];W[or];B[ms];W[go];B[gr];W[pp]
LB[lq:A][fo:B]
C[If Black cuts at A, White can trade to the LR corner, with the endgame move at B as a bonus. This makes the game close. ]
))
(;W[rg];B[si];W[pg];B[mh];W[ng];B[og];W[nh];B[oh];W[ph];B[ni];W[mi];B[nj]
;W[lh];B[mj];W[li];B[qf]LB[mh:A]
C[After Black A, White has no way to hold things together. An easy win for Black. ]
))
(;W[ne];B[nf];W[lf];B[nh]LB[ke:A][nc:B]
C[Black's group on the right has escaped into the center, and the aji at A and B is troublesome for White already. ]
))
(;W[he];B[id];W[kd];B[ib];W[hb];B[ic];W[kb];B[ge];W[gf];B[gb];W[fb];B[ga]
;W[ha];B[ia];W[fa];B[gb];W[hc];B[fd];W[jc];B[hd];W[ge];B[gh]
C[Black has an advantage in this fight on the upper side, and in this variation wins by one move. ]
))
(;W[pj];B[rk];W[qh];B[rh];W[pi];B[ri];W[eq]LB[pj:A][ie:B]
C[Pulling back at A was the normal move for White, and White has succeeded in reinforcing the center while pushing Black down to the second line. I think Black still has a slight lead with Black B next. ]
))
(;W[bh];B[bi];W[bf];B[ah];W[af];B[bj];W[bm];B[ag];W[aj];B[ae]LB[bj:A]
C[With the forcing move at A, Black gets an extra liberty to win the semeai. ]
))
(;B[em];W[bh];B[cf];W[bg];B[gl];W[gj];B[hj];W[bj];B[ke]LB[em:A][bh:B]
C[When Black extends at A, White can win the semeai with B. Actually, this is not bad for Black, but it does not seem to be Shuwa's style to give White territory on the left when he can win the semai as in the game sequence. ]
))
(;W[dg];B[cg];W[df];B[ed];W[dm];B[em];W[dn];B[gl];W[bf];B[be];W[cf];B[bg]
;W[ag];B[ah];W[af];B[gj];W[ai];B[fk];W[bh];B[ae]LB[dg:A][ed:B][dm:C][gl:D]
C[If White pushes through at A, Black can crawl once and simply connect at B. After White plays C, Black can capture the other side group with D as shown, so the two points were miai to give Black a win. ]
))
(;B[cl];W[bk];B[el];W[ch]LB[cl:A]
C[Cutting at A is bad when the ladder favors White. ]))
(;B[ck];W[ek]LB[dl:A][ck:B]
C[The exchange of A for B was a big kikashi for White, and White has an advantage. ]
))
(;W[dd];B[dc];W[gh]LB[ed:A]
C[Since White is threatening to cut Black off at A, Black cannot break through in the center. This board position is close to even, without needing any komi.]
))
(;B[ke];W[kc];B[me];W[md];B[ne]LB[ke:A][me:B]
C[When Black invades at A, White will have trouble attacking because of the weakness at B. Black can play B immediately as shown, or play away after the original exchange, which is a kikashi for Black. ]
))
(;B[fd];W[gd];B[fe];W[ge];B[ff];W[hg]LB[fd:A]
C[Black A was the natural looking move. While White gets a nice moyo on the upper side, Black still has a distinct advantage when there is no komi. ]
))
(;B[qc];W[pd];B[pe];W[qe]LB[of:A]
C[When the ladder at A favors White, this is generally considered to be bad for Black. ]
))
(;W[pd];B[pe];W[pc];B[qe];W[oe];B[nd];W[qc];B[qf];W[of];B[qh];W[mb];B[ld]
;W[oh];B[pj];W[lg];B[jd]LB[ec:A][of:B][ne:C][pg:D]
C[This variation shows a basic variation of the Taisha joseki. Black A is well placed in relation to the Black group on the upper side, and this board position is good for Black.
A major diverging point in this joseki was White's connection at B, where White could have pushed at C or extended at D, with a multitude of complicated variations following. ]
))

View File

@ -0,0 +1 @@
(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[7.5]SZ[19]DT[2024-03-24];B[pd](;W[dd];B[op];W[dp])(;W[nc];B[dp]))