use crate::token::Action::{Move, Pass};
use crate::token::Color::{Black, White};
use crate::token::Outcome::{Draw, WinnerByForfeit, WinnerByPoints, WinnerByResign, WinnerByTime};
use crate::{SgfError, SgfErrorKind};
use std::ops::Not;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Color {
Black,
White,
}
impl Not for Color {
type Output = Color;
fn not(self) -> Color {
match self {
Color::Black => Color::White,
Color::White => Color::Black,
}
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Outcome {
WinnerByResign(Color),
WinnerByForfeit(Color),
WinnerByPoints(Color, f32),
WinnerByTime(Color),
Draw,
}
impl Outcome {
pub fn get_winner(self) -> Option<Color> {
match self {
WinnerByTime(color)
| WinnerByForfeit(color)
| WinnerByPoints(color, ..)
| WinnerByResign(color) => Some(color),
_ => None,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Action {
Move(u8, u8),
Pass,
}
#[derive(Debug, PartialEq, Clone)]
pub enum SgfToken {
Add { color: Color, coordinate: (u8, u8) },
Move { color: Color, action: Action },
Time { color: Color, time: u32 },
PlayerName { color: Color, name: String },
PlayerRank { color: Color, rank: String },
Result(Outcome),
Komi(f32),
Event(String),
Copyright(String),
GameName(String),
Place(String),
Date(String),
Size(u32, u32),
TimeLimit(u32),
Handicap(u32),
Comment(String),
Unknown((String, String)),
Invalid((String, String)),
Square { coordinate: (u8, u8) },
Triangle { coordinate: (u8, u8) },
Label { label: String, coordinate: (u8, u8) },
}
impl SgfToken {
pub fn from_pair(base_ident: &str, value: &str) -> SgfToken {
let ident = base_ident
.chars()
.filter(|c| c.is_uppercase())
.collect::<String>();
let token: Option<SgfToken> = match ident.as_ref() {
"LB" => split_label_text(value).and_then(|(coord, label)| {
str_to_coordinates(coord)
.ok()
.map(|coordinate| SgfToken::Label {
label: label[1..].to_string(),
coordinate,
})
}),
"HA" => match value.parse() {
Ok(value) => Some(SgfToken::Handicap(value)),
_ => None,
},
"SQ" => str_to_coordinates(value)
.ok()
.map(|coordinate| SgfToken::Square { coordinate }),
"TR" => str_to_coordinates(value)
.ok()
.map(|coordinate| SgfToken::Triangle { coordinate }),
"AB" => str_to_coordinates(value)
.ok()
.map(|coordinate| SgfToken::Add {
color: Color::Black,
coordinate,
}),
"B" => move_str_to_coord(value)
.ok()
.map(|coordinate| SgfToken::Move {
color: Color::Black,
action: coordinate,
}),
"BL" => value.parse().ok().map(|time| SgfToken::Time {
color: Color::Black,
time,
}),
"PB" => Some(SgfToken::PlayerName {
color: Color::Black,
name: value.to_string(),
}),
"BR" => Some(SgfToken::PlayerRank {
color: Color::Black,
rank: value.to_string(),
}),
"AW" => str_to_coordinates(value)
.ok()
.map(|coordinate| SgfToken::Add {
color: Color::White,
coordinate,
}),
"W" => move_str_to_coord(value)
.ok()
.map(|coordinate| SgfToken::Move {
color: Color::White,
action: coordinate,
}),
"WL" => value.parse().ok().map(|time| SgfToken::Time {
color: Color::White,
time,
}),
"PW" => Some(SgfToken::PlayerName {
color: Color::White,
name: value.to_string(),
}),
"WR" => Some(SgfToken::PlayerRank {
color: Color::White,
rank: value.to_string(),
}),
"RE" => parse_outcome_str(value).ok().map(SgfToken::Result),
"KM" => value.parse().ok().map(SgfToken::Komi),
"SZ" => {
if let Some((width, height)) = split_size_text(value) {
Some(SgfToken::Size(width, height))
} else {
value.parse().ok().map(|size| SgfToken::Size(size, size))
}
}
"TM" => value.parse().ok().map(SgfToken::TimeLimit),
"EV" => Some(SgfToken::Event(value.to_string())),
"C" => Some(SgfToken::Comment(value.to_string())),
"GN" => Some(SgfToken::GameName(value.to_string())),
"CR" => Some(SgfToken::Copyright(value.to_string())),
"DT" => Some(SgfToken::Date(value.to_string())),
"PC" => Some(SgfToken::Place(value.to_string())),
_ => Some(SgfToken::Unknown((
base_ident.to_string(),
value.to_string(),
))),
};
match token {
Some(token) => token,
_ => SgfToken::Invalid((base_ident.to_string(), value.to_string())),
}
}
pub fn is_root_token(&self) -> bool {
use SgfToken::*;
match self {
Size(_, _) => true,
_ => false,
}
}
}
impl Into<String> for &SgfToken {
fn into(self) -> String {
match self {
SgfToken::Label { label, coordinate } => {
let value = coordinate_to_str(*coordinate);
format!("LB[{}:{}]", value, label)
}
SgfToken::Handicap(nb_stones) => format!("HA[{}]", nb_stones),
SgfToken::Result(outcome) => match outcome {
WinnerByPoints(color, points) => format!(
"RE[{}+{}]",
match color {
Black => "B",
White => "W",
},
points
),
WinnerByResign(color) => format!(
"RE[{}+R]",
match color {
Black => "B",
White => "W",
}
),
WinnerByTime(color) => format!(
"RE[{}+T]",
match color {
Black => "B",
White => "W",
}
),
WinnerByForfeit(color) => format!(
"RE[{}+F]",
match color {
Black => "B",
White => "W",
}
),
Draw => "RE[Draw]".to_string(),
},
SgfToken::Square { coordinate } => {
let value = coordinate_to_str(*coordinate);
format!("SQ[{}]", value)
}
SgfToken::Triangle { coordinate } => {
let value = coordinate_to_str(*coordinate);
format!("TR[{}]", value)
}
SgfToken::Add { color, coordinate } => {
let token = match color {
Color::Black => "AB",
Color::White => "AW",
};
let value = coordinate_to_str(*coordinate);
format!("{}[{}]", token, value)
}
SgfToken::Move { color, action } => {
let token = match color {
Color::Black => "B",
Color::White => "W",
};
let value = match *action {
Move(x, y) => coordinate_to_str((x, y)),
Pass => String::new(),
};
format!("{}[{}]", token, value)
}
SgfToken::Time { color, time } => {
let token = match color {
Color::Black => "BL",
Color::White => "WL",
};
format!("{}[{}]", token, time)
}
SgfToken::PlayerName { color, name } => {
let token = match color {
Color::Black => "PB",
Color::White => "PW",
};
format!("{}[{}]", token, name)
}
SgfToken::PlayerRank { color, rank } => {
let token = match color {
Color::Black => "BR",
Color::White => "WR",
};
format!("{}[{}]", token, rank)
}
SgfToken::Komi(komi) => format!("KM[{}]", komi),
SgfToken::Size(width, height) if width == height => format!("SZ[{}]", width),
SgfToken::Size(width, height) => format!("SZ[{}:{}]", width, height),
SgfToken::TimeLimit(time) => format!("TM[{}]", time),
SgfToken::Event(value) => format!("EV[{}]", value),
SgfToken::Comment(value) => format!("C[{}]", value),
SgfToken::GameName(value) => format!("GN[{}]", value),
SgfToken::Copyright(value) => format!("CR[{}]", value),
SgfToken::Date(value) => format!("DT[{}]", value),
SgfToken::Place(value) => format!("PC[{}]", value),
_ => panic!(),
}
}
}
impl Into<String> for SgfToken {
fn into(self) -> String {
(&self).into()
}
}
fn split_size_text(input: &str) -> Option<(u32, u32)> {
let index = input.find(':')?;
let (width_part, height_part) = input.split_at(index);
let width: u32 = width_part.parse().ok()?;
let height: u32 = height_part[1..].parse().ok()?;
Some((width, height))
}
fn coordinate_to_str(coordinate: (u8, u8)) -> String {
let x = (coordinate.0 + 96) as char;
let y = (coordinate.1 + 96) as char;
format!("{}{}", x, y)
}
fn split_label_text(input: &str) -> Option<(&str, &str)> {
if input.len() >= 4 {
Some(input.split_at(2))
} else {
None
}
}
fn parse_outcome_str(s: &str) -> Result<Outcome, SgfError> {
if s.is_empty() || s == "Void" {
return Err(SgfError::from(SgfErrorKind::ParseError));
}
if s == "Draw" || s == "D" {
return Ok(Draw);
}
let winner_option: Vec<&str> = s.split('+').collect();
if winner_option.len() != 2 {
return Err(SgfError::from(SgfErrorKind::ParseError));
}
let winner: Color = match &winner_option[0] as &str {
"B" => Black,
"W" => White,
_ => return Err(SgfError::from(SgfErrorKind::ParseError)),
};
match &winner_option[1] as &str {
"F" | "Forfeit" => Ok(WinnerByForfeit(winner)),
"R" | "Resign" => Ok(WinnerByResign(winner)),
"T" | "Time" => Ok(WinnerByTime(winner)),
points => {
if let Ok(outcome) = points
.parse::<f32>()
.map(|score| WinnerByPoints(winner, score))
{
Ok(outcome)
} else {
Err(SgfError::from(SgfErrorKind::ParseError))
}
}
}
}
fn move_str_to_coord(input: &str) -> Result<Action, SgfError> {
if input.is_empty() {
Ok(Pass)
} else {
match str_to_coordinates(input) {
Ok(coordinates) => Ok(Move(coordinates.0, coordinates.1)),
Err(e) => Err(e),
}
}
}
fn str_to_coordinates(input: &str) -> Result<(u8, u8), SgfError> {
if input.len() != 2 {
Err(SgfErrorKind::ParseError.into())
} else {
let coords = input
.to_lowercase()
.as_bytes()
.iter()
.map(|c| convert_u8_to_coordinate(*c))
.collect::<Vec<_>>();
Ok((coords[0], coords[1]))
}
}
#[inline]
fn convert_u8_to_coordinate(c: u8) -> u8 {
c - 96
}