Rebuild a game board from SGF #68
|
@ -7,8 +7,7 @@
|
||||||
;W[fd]
|
;W[fd]
|
||||||
;B[qf]
|
;B[qf]
|
||||||
;W[qh]
|
;W[qh]
|
||||||
(;B[pf]
|
(;B[of]
|
||||||
)(;B[of]
|
|
||||||
;W[nd]
|
;W[nd]
|
||||||
;B[mf]
|
;B[mf]
|
||||||
;W[pk]
|
;W[pk]
|
||||||
|
@ -221,4 +220,5 @@
|
||||||
;B[ai]
|
;B[ai]
|
||||||
;W[qg]
|
;W[qg]
|
||||||
;B[pf]
|
;B[pf]
|
||||||
|
)(;B[pf]
|
||||||
))
|
))
|
|
@ -1,3 +1,5 @@
|
||||||
|
use sgf::{go::Game, parse_sgf};
|
||||||
|
|
||||||
use crate::{BoardError, Color, Size};
|
use crate::{BoardError, Color, Size};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
@ -77,6 +79,41 @@ pub struct Coordinate {
|
||||||
pub row: u8,
|
pub row: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Coordinate {
|
||||||
|
fn from_sgf(s: &str) -> Self {
|
||||||
|
fn parse(s: char) -> u8 {
|
||||||
|
match s {
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
'h' => 7,
|
||||||
|
'i' => 8,
|
||||||
|
'j' => 9,
|
||||||
|
'k' => 10,
|
||||||
|
'l' => 11,
|
||||||
|
'm' => 12,
|
||||||
|
'n' => 13,
|
||||||
|
'o' => 14,
|
||||||
|
'p' => 15,
|
||||||
|
'q' => 16,
|
||||||
|
'r' => 17,
|
||||||
|
's' => 18,
|
||||||
|
_ => panic!("invalid character in the SGF coordinates"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = s.chars().collect::<Vec<char>>();
|
||||||
|
Coordinate {
|
||||||
|
column: parse(s[0]),
|
||||||
|
row: parse(s[1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||||
if let Some(_) = self.stone(&coordinate) {
|
if let Some(_) = self.stone(&coordinate) {
|
||||||
|
@ -123,17 +160,17 @@ impl Board {
|
||||||
.map(|g| g.color)
|
.map(|g| g.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.coordinates.contains(coordinate))
|
.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);
|
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 adjacent_spaces = self.group_halo(group).into_iter();
|
||||||
let mut grps: Vec<Group> = Vec::new();
|
let mut grps: Vec<Group> = Vec::new();
|
||||||
|
|
||||||
|
@ -153,7 +190,7 @@ impl Board {
|
||||||
grps
|
grps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
||||||
group
|
group
|
||||||
.coordinates
|
.coordinates
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -162,14 +199,14 @@ impl Board {
|
||||||
.collect::<HashSet<Coordinate>>()
|
.collect::<HashSet<Coordinate>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn liberties(&self, group: &Group) -> usize {
|
fn liberties(&self, group: &Group) -> usize {
|
||||||
self.group_halo(group)
|
self.group_halo(group)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(&c) == None)
|
.filter(|c| self.stone(&c) == None)
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
if coordinate.column > 0 {
|
if coordinate.column > 0 {
|
||||||
v.push(Coordinate {
|
v.push(Coordinate {
|
||||||
|
@ -194,7 +231,7 @@ impl Board {
|
||||||
v.into_iter().filter(|c| self.within_board(c)).collect()
|
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
|
coordinate.column < self.size.width && coordinate.row < self.size.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,9 +248,37 @@ impl Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Game> for Board {
|
||||||
|
type Error = BoardError;
|
||||||
|
fn try_from(record: &Game) -> Result<Self, Self::Error> {
|
||||||
|
let mut board = Board::new();
|
||||||
|
let mut root = Some(&record.root);
|
||||||
|
while let Some(node) = root {
|
||||||
|
match (node.find_prop("B"), node.find_prop("W")) {
|
||||||
|
(Some(prop), _) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::Black)?;
|
||||||
|
}
|
||||||
|
(None, Some(prop)) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::White)?;
|
||||||
|
}
|
||||||
|
(None, None) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
root = node.next()
|
||||||
|
}
|
||||||
|
Ok(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use sgf::{parse_sgf, Game};
|
||||||
|
|
||||||
/* Two players (Black and White) take turns and Black plays first
|
/* Two players (Black and White) take turns and Black plays first
|
||||||
* Stones are placed on the line intersections and not moved.
|
* Stones are placed on the line intersections and not moved.
|
||||||
|
@ -225,6 +290,25 @@ mod test {
|
||||||
* A stone placed in a suicidal position is legal if it captures other stones first.
|
* A stone placed in a suicidal position is legal if it captures other stones first.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fn with_text(text: &str, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let games = parse_sgf(text)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|game| match game {
|
||||||
|
Game::Go(g) => Some(g),
|
||||||
|
Game::Unsupported(_) => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<sgf::go::Game>>();
|
||||||
|
f(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let mut file = File::open(path).unwrap();
|
||||||
|
let mut text = String::new();
|
||||||
|
let _ = file.read_to_string(&mut text);
|
||||||
|
with_text(&text, f);
|
||||||
|
}
|
||||||
|
|
||||||
fn with_example_board(test: impl FnOnce(Board)) {
|
fn with_example_board(test: impl FnOnce(Board)) {
|
||||||
let board = Board::from_coordinates(
|
let board = Board::from_coordinates(
|
||||||
vec![
|
vec![
|
||||||
|
@ -630,4 +714,46 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(board, b2);
|
assert_eq!(board, b2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_board_from_sgf() {
|
||||||
|
with_file(
|
||||||
|
std::path::Path::new("fixtures/five_games/2022.10.05.sgf"),
|
||||||
|
|trees| {
|
||||||
|
let game = &trees[0];
|
||||||
|
let board = Board::try_from(game).expect("game to be valid");
|
||||||
|
assert_eq!(
|
||||||
|
board.stone(&Coordinate { column: 14, row: 5 }),
|
||||||
|
Some(Color::Black)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* This game has a fork in the game tree here. This block verifies that we have
|
||||||
|
* reached the move right before the fork. */
|
||||||
|
let mut node = &game.root;
|
||||||
|
for _ in 0..8 {
|
||||||
|
node = node.next().unwrap();
|
||||||
|
}
|
||||||
|
let prop = assert_matches!(node.find_prop("W"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["qh".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* And now we verify that we have gone down the leftmost side of the fork, which is
|
||||||
|
* traditionally the mainline of the game. */
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let prop = assert_matches!(node.find_prop("B"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["of".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ mod test {
|
||||||
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
||||||
assert_eq!(game.info.white_player, Some("Savanni".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.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
||||||
assert_eq!(game.info.komi, Some(6.5));
|
// assert_eq!(game.info.komi, Some(6.5));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
"name":"Savanni",
|
"name":"Savanni",
|
||||||
"rank":{"Kyu":10}
|
"rank":{"Kyu":10}
|
||||||
},
|
},
|
||||||
"DatabasePath": "/home/savanni/Documents/50 Ludoj/53 Kifu"
|
"DatabasePath": "kifu/core/fixtures/five_games/"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod go;
|
||||||
|
|
||||||
mod tree;
|
mod tree;
|
||||||
use tree::parse_collection;
|
use tree::parse_collection;
|
||||||
|
pub use tree::Property;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue