1use crate::token::Action::{Move, Pass};
2use crate::token::Color::{Black, White};
3use crate::token::Outcome::{Draw, WinnerByForfeit, WinnerByPoints, WinnerByResign, WinnerByTime};
4use crate::{SgfError, SgfErrorKind};
5use std::ops::Not;
6
7#[derive(Debug, PartialEq, Eq, Copy, Clone)]
9pub enum Color {
10 Black,
11 White,
12}
13
14impl Not for Color {
15 type Output = Color;
16 fn not(self) -> Color {
17 match self {
18 Color::Black => Color::White,
19 Color::White => Color::Black,
20 }
21 }
22}
23
24#[derive(Debug, PartialEq, Copy, Clone)]
25pub enum Outcome {
26 WinnerByResign(Color),
27 WinnerByForfeit(Color),
28 WinnerByPoints(Color, f32),
29 WinnerByTime(Color),
30 Draw,
31}
32
33impl Outcome {
34 pub fn get_winner(self) -> Option<Color> {
35 match self {
36 WinnerByTime(color)
37 | WinnerByForfeit(color)
38 | WinnerByPoints(color, ..)
39 | WinnerByResign(color) => Some(color),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum RuleSet {
57 Japanese,
58 NZ,
59 GOE,
60 AGA,
61 Chinese,
62 Unknown(String),
63}
64
65impl From<&str> for RuleSet {
66 fn from(s: &str) -> Self {
67 match s {
68 "Japanese" => RuleSet::Japanese,
69 "AGA" => RuleSet::AGA,
70 "NZ" => RuleSet::NZ,
71 "Chinese" => RuleSet::Chinese,
72 "GOE" => RuleSet::GOE,
73 value => RuleSet::Unknown(value.to_owned()),
74 }
75 }
76}
77
78impl ToString for RuleSet {
79 fn to_string(&self) -> String {
80 match self {
81 RuleSet::Japanese => "Japanese",
82 RuleSet::NZ => "NZ",
83 RuleSet::GOE => "GOE",
84 RuleSet::AGA => "AGA",
85 RuleSet::Chinese => "Chinese",
86 RuleSet::Unknown(v) => v,
87 }
88 .to_owned()
89 }
90}
91
92#[derive(Copy, Clone, Eq, PartialEq, Debug)]
93pub enum Action {
94 Move(u8, u8),
95 Pass,
96}
97
98#[derive(Copy, Clone, Eq, PartialEq, Debug)]
99pub enum Game {
100 Go,
101 Other(u8),
102}
103
104#[derive(Clone, Eq, PartialEq, Debug)]
105pub enum Encoding {
106 UTF8,
107 Other(String),
108}
109
110#[derive(Copy, Clone, Eq, PartialEq, Debug)]
111pub enum DisplayNodes {
112 Children,
113 Siblings,
114}
115
116#[derive(Debug, PartialEq, Clone)]
118pub enum SgfToken {
119 Add {
120 color: Color,
121 coordinate: (u8, u8),
122 },
123 Move {
124 color: Color,
125 action: Action,
126 },
127 Time {
128 color: Color,
129 time: u32,
130 },
131 PlayerName {
132 color: Color,
133 name: String,
134 },
135 PlayerRank {
136 color: Color,
137 rank: String,
138 },
139 Game(Game),
140 Rule(RuleSet),
141 Result(Outcome),
142 Komi(f32),
143 Event(String),
144 Copyright(String),
145 GameName(String),
146 VariationDisplay {
147 nodes: DisplayNodes,
148 on_board_display: bool,
149 },
150 Place(String),
151 Date(String),
152 Size(u32, u32),
153 FileFormat(u8),
154 Overtime(String),
155 TimeLimit(u32),
156 MovesRemaining {
157 color: Color,
158 moves: u32,
159 },
160 Handicap(u32),
161 Comment(String),
162 Charset(Encoding),
163 Application {
164 name: String,
165 version: String,
166 },
167 Unknown((String, String)),
168 Invalid((String, String)),
169 Square {
170 coordinate: (u8, u8),
171 },
172 Triangle {
173 coordinate: (u8, u8),
174 },
175 Label {
176 label: String,
177 coordinate: (u8, u8),
178 },
179}
180
181impl SgfToken {
182 pub fn from_pair(base_ident: &str, value: &str) -> SgfToken {
204 let ident = base_ident
205 .chars()
206 .filter(|c| c.is_uppercase())
207 .collect::<String>();
208 let token: Option<SgfToken> = match ident.as_ref() {
209 "LB" => split_label_text(value).and_then(|(coord, label)| {
210 str_to_coordinates(coord)
211 .ok()
212 .map(|coordinate| SgfToken::Label {
213 label: label[1..].to_string(),
214 coordinate,
215 })
216 }),
217 "HA" => match value.parse() {
218 Ok(value) => Some(SgfToken::Handicap(value)),
219 _ => None,
220 },
221 "RU" => Some(SgfToken::Rule(RuleSet::from(value))),
222 "SQ" => str_to_coordinates(value)
223 .ok()
224 .map(|coordinate| SgfToken::Square { coordinate }),
225 "TR" => str_to_coordinates(value)
226 .ok()
227 .map(|coordinate| SgfToken::Triangle { coordinate }),
228 "AB" => str_to_coordinates(value)
229 .ok()
230 .map(|coordinate| SgfToken::Add {
231 color: Color::Black,
232 coordinate,
233 }),
234 "B" => move_str_to_coord(value)
235 .ok()
236 .map(|coordinate| SgfToken::Move {
237 color: Color::Black,
238 action: coordinate,
239 }),
240 "BL" => value.parse().ok().map(|time| SgfToken::Time {
241 color: Color::Black,
242 time,
243 }),
244 "PB" => Some(SgfToken::PlayerName {
245 color: Color::Black,
246 name: value.to_string(),
247 }),
248 "BR" => Some(SgfToken::PlayerRank {
249 color: Color::Black,
250 rank: value.to_string(),
251 }),
252 "AW" => str_to_coordinates(value)
253 .ok()
254 .map(|coordinate| SgfToken::Add {
255 color: Color::White,
256 coordinate,
257 }),
258 "W" => move_str_to_coord(value)
259 .ok()
260 .map(|coordinate| SgfToken::Move {
261 color: Color::White,
262 action: coordinate,
263 }),
264 "WL" => value.parse().ok().map(|time| SgfToken::Time {
265 color: Color::White,
266 time,
267 }),
268 "PW" => Some(SgfToken::PlayerName {
269 color: Color::White,
270 name: value.to_string(),
271 }),
272 "WR" => Some(SgfToken::PlayerRank {
273 color: Color::White,
274 rank: value.to_string(),
275 }),
276 "RE" => parse_outcome_str(value).ok().map(SgfToken::Result),
277 "KM" => value.parse().ok().map(SgfToken::Komi),
278 "SZ" => {
279 if let Some((width, height)) = split_size_text(value) {
280 Some(SgfToken::Size(width, height))
281 } else {
282 value.parse().ok().map(|size| SgfToken::Size(size, size))
283 }
284 }
285 "FF" => value.parse().ok().map(|v| match v {
286 0..=4 => SgfToken::FileFormat(v),
287 _ => SgfToken::Invalid((ident.to_string(), value.to_string())),
288 }),
289 "TM" => value.parse().ok().map(SgfToken::TimeLimit),
290 "EV" => Some(SgfToken::Event(value.to_string())),
291 "OT" => Some(SgfToken::Overtime(value.to_string())),
292 "C" => Some(SgfToken::Comment(value.to_string())),
293 "GN" => Some(SgfToken::GameName(value.to_string())),
294 "CR" => Some(SgfToken::Copyright(value.to_string())),
295 "DT" => Some(SgfToken::Date(value.to_string())),
296 "PC" => Some(SgfToken::Place(value.to_string())),
297 "GM" => match value.parse::<u8>() {
298 Ok(1) => Some(SgfToken::Game(Game::Go)),
299 Ok(n) => Some(SgfToken::Game(Game::Other(n))),
300 Err(_) => Some(SgfToken::Invalid((
301 base_ident.to_string(),
302 value.to_string(),
303 ))),
304 },
305 "CA" => match value.to_string().to_lowercase().as_str() {
306 "utf-8" => Some(SgfToken::Charset(Encoding::UTF8)),
307 _ => Some(SgfToken::Charset(Encoding::Other(value.to_string()))),
308 },
309 "OB" => match value.parse::<u32>() {
310 Ok(n) => Some(SgfToken::MovesRemaining {
311 color: Color::Black,
312 moves: n,
313 }),
314 Err(_) => Some(SgfToken::Invalid((
315 base_ident.to_string(),
316 value.to_string(),
317 ))),
318 },
319 "OW" => match value.parse::<u32>() {
320 Ok(n) => Some(SgfToken::MovesRemaining {
321 color: Color::White,
322 moves: n,
323 }),
324 Err(_) => Some(SgfToken::Invalid((
325 base_ident.to_string(),
326 value.to_string(),
327 ))),
328 },
329 "AP" => parse_application_str(value)
330 .ok()
331 .map(|(name, version)| SgfToken::Application { name, version }),
332 "ST" => parse_variation_display_str(value)
333 .ok()
334 .map(|(nodes, on_board_display)| SgfToken::VariationDisplay {
335 nodes,
336 on_board_display,
337 }),
338 _ => Some(SgfToken::Unknown((
339 base_ident.to_string(),
340 value.to_string(),
341 ))),
342 };
343 match token {
344 Some(token) => token,
345 _ => SgfToken::Invalid((base_ident.to_string(), value.to_string())),
346 }
347 }
348
349 pub fn is_root_token(&self) -> bool {
364 use SgfToken::*;
365 matches!(
366 self,
367 Size(_, _)
368 | Charset(_)
369 | FileFormat(_)
370 | Game(_)
371 | VariationDisplay { .. }
372 | Application { .. }
373 )
374 }
375
376 pub fn is_setup_token(&self) -> bool {
390 use SgfToken::*;
391 matches!(self, Add { .. })
392 }
393
394 pub fn is_game_info_token(&self) -> bool {
409 use SgfToken::*;
410 matches!(
411 self,
412 Date(_)
413 | GameName(_)
414 | Handicap(_)
415 | Komi(_)
416 | Overtime(_)
417 | Event(_)
418 | Result(_)
419 | Rule(_)
420 | Place(_)
421 | TimeLimit(_)
422 | PlayerName { .. }
423 | PlayerRank { .. }
424 | Copyright(_)
425 )
426 }
427}
428
429impl Into<String> for &SgfToken {
430 fn into(self) -> String {
431 match self {
432 SgfToken::Label { label, coordinate } => {
433 let value = coordinate_to_str(*coordinate);
434 format!("LB[{}:{}]", value, label)
435 }
436 SgfToken::Handicap(nb_stones) => format!("HA[{}]", nb_stones),
437 SgfToken::Rule(rule) => format!("RU[{}]", rule.to_string()),
438 SgfToken::Result(outcome) => match outcome {
439 WinnerByPoints(color, points) => format!(
440 "RE[{}+{}]",
441 match color {
442 Black => "B",
443 White => "W",
444 },
445 points
446 ),
447 WinnerByResign(color) => format!(
448 "RE[{}+R]",
449 match color {
450 Black => "B",
451 White => "W",
452 }
453 ),
454
455 WinnerByTime(color) => format!(
456 "RE[{}+T]",
457 match color {
458 Black => "B",
459 White => "W",
460 }
461 ),
462 WinnerByForfeit(color) => format!(
463 "RE[{}+F]",
464 match color {
465 Black => "B",
466 White => "W",
467 }
468 ),
469 Draw => "RE[Draw]".to_string(),
470 },
471 SgfToken::Square { coordinate } => {
472 let value = coordinate_to_str(*coordinate);
473 format!("SQ[{}]", value)
474 }
475 SgfToken::Triangle { coordinate } => {
476 let value = coordinate_to_str(*coordinate);
477 format!("TR[{}]", value)
478 }
479 SgfToken::Add { color, coordinate } => {
480 let token = match color {
481 Color::Black => "AB",
482 Color::White => "AW",
483 };
484 let value = coordinate_to_str(*coordinate);
485 format!("{}[{}]", token, value)
486 }
487 SgfToken::Move { color, action } => {
488 let token = match color {
489 Color::Black => "B",
490 Color::White => "W",
491 };
492 let value = match *action {
493 Move(x, y) => coordinate_to_str((x, y)),
494 Pass => String::new(),
495 };
496 format!("{}[{}]", token, value)
497 }
498 SgfToken::Time { color, time } => {
499 let token = match color {
500 Color::Black => "BL",
501 Color::White => "WL",
502 };
503 format!("{}[{}]", token, time)
504 }
505 SgfToken::PlayerName { color, name } => {
506 let token = match color {
507 Color::Black => "PB",
508 Color::White => "PW",
509 };
510 format!("{}[{}]", token, name)
511 }
512 SgfToken::PlayerRank { color, rank } => {
513 let token = match color {
514 Color::Black => "BR",
515 Color::White => "WR",
516 };
517 format!("{}[{}]", token, rank)
518 }
519 SgfToken::Komi(komi) => format!("KM[{}]", komi),
520 SgfToken::FileFormat(v) => format!("FF[{}]", v),
521 SgfToken::Size(width, height) if width == height => format!("SZ[{}]", width),
522 SgfToken::Size(width, height) => format!("SZ[{}:{}]", width, height),
523 SgfToken::TimeLimit(time) => format!("TM[{}]", time),
524 SgfToken::Event(value) => format!("EV[{}]", value),
525 SgfToken::Comment(value) => format!("C[{}]", value),
526 SgfToken::Overtime(value) => format!("OT[{}]", value),
527 SgfToken::GameName(value) => format!("GN[{}]", value),
528 SgfToken::Copyright(value) => format!("CR[{}]", value),
529 SgfToken::Date(value) => format!("DT[{}]", value),
530 SgfToken::Place(value) => format!("PC[{}]", value),
531 SgfToken::Game(game) => format!(
532 "GM[{}]",
533 match game {
534 Game::Go => &1u8,
535 Game::Other(n) => n,
536 }
537 ),
538 SgfToken::Charset(_) => "CA[UTF-8]".to_string(),
539 SgfToken::MovesRemaining { color, moves } => format!(
540 "O{}[{}]",
541 match color {
542 Color::Black => 'B',
543 Color::White => 'W',
544 },
545 moves
546 ),
547 SgfToken::VariationDisplay {
548 nodes,
549 on_board_display,
550 } => {
551 let num = match (nodes, on_board_display) {
552 (DisplayNodes::Children, true) => 0,
553 (DisplayNodes::Siblings, true) => 1,
554 (DisplayNodes::Children, false) => 2,
555 (DisplayNodes::Siblings, false) => 3,
556 };
557 format!("ST[{}]", num)
558 }
559 SgfToken::Application { name, version } => format!("AP[{}:{}]", name, version),
560 SgfToken::Unknown((ident, prop)) => format!("{}[{}]", ident, prop),
561 SgfToken::Invalid((ident, prop)) => format!("{}[{}]", ident, prop),
562 }
563 }
564}
565
566impl Into<String> for SgfToken {
567 fn into(self) -> String {
568 (&self).into()
569 }
570}
571
572fn split_size_text(input: &str) -> Option<(u32, u32)> {
574 let index = input.find(':')?;
575 let (width_part, height_part) = input.split_at(index);
576 let width: u32 = width_part.parse().ok()?;
577 let height: u32 = height_part[1..].parse().ok()?;
578 Some((width, height))
579}
580
581fn coordinate_to_str(coordinate: (u8, u8)) -> String {
583 fn to_char(c: u8) -> char {
584 (c + if c < 27 { 96 } else { 38 }) as char
585 }
586
587 let x = to_char(coordinate.0);
588 let y = to_char(coordinate.1);
589
590 format!("{}{}", x, y)
591}
592
593fn split_label_text(input: &str) -> Option<(&str, &str)> {
595 if input.len() >= 4 {
596 Some(input.split_at(2))
597 } else {
598 None
599 }
600}
601
602fn parse_variation_display_str(input: &str) -> Result<(DisplayNodes, bool), SgfError> {
603 match input.parse::<u8>() {
604 Ok(0) => Ok((DisplayNodes::Children, true)),
605 Ok(1) => Ok((DisplayNodes::Siblings, true)),
606 Ok(2) => Ok((DisplayNodes::Children, false)),
607 Ok(3) => Ok((DisplayNodes::Siblings, false)),
608 _ => Err(SgfError::from(SgfErrorKind::ParseError)),
609 }
610}
611
612fn parse_application_str(input: &str) -> Result<(String, String), SgfError> {
613 let index = input
614 .find(':')
615 .ok_or_else(|| SgfError::from(SgfErrorKind::ParseError))?;
616 let (name, version) = input.split_at(index);
617 Ok((name.to_string(), version[1..].to_string()))
618}
619
620fn parse_outcome_str(s: &str) -> Result<Outcome, SgfError> {
635 if s.is_empty() || s == "Void" {
636 return Err(SgfError::from(SgfErrorKind::ParseError));
637 }
638 if s == "Draw" || s == "D" {
639 return Ok(Draw);
640 }
641
642 let winner_option: Vec<&str> = s.split('+').collect();
643 if winner_option.len() != 2 {
644 return Err(SgfError::from(SgfErrorKind::ParseError));
645 }
646
647 let winner: Color = match &winner_option[0] as &str {
648 "B" => Black,
649 "W" => White,
650 _ => return Err(SgfError::from(SgfErrorKind::ParseError)),
651 };
652
653 match &winner_option[1] as &str {
654 "F" | "Forfeit" => Ok(WinnerByForfeit(winner)),
655 "R" | "Resign" => Ok(WinnerByResign(winner)),
656 "T" | "Time" => Ok(WinnerByTime(winner)),
657 points => {
658 if let Ok(outcome) = points
659 .parse::<f32>()
660 .map(|score| WinnerByPoints(winner, score))
661 {
662 Ok(outcome)
663 } else {
664 Err(SgfError::from(SgfErrorKind::ParseError))
665 }
666 }
667 }
668}
669
670fn move_str_to_coord(input: &str) -> Result<Action, SgfError> {
671 if input.is_empty() {
672 Ok(Pass)
673 } else {
674 match str_to_coordinates(input) {
675 Ok(coordinates) => Ok(Move(coordinates.0, coordinates.1)),
676 Err(e) => Err(e),
677 }
678 }
679}
680
681fn str_to_coordinates(input: &str) -> Result<(u8, u8), SgfError> {
683 if input.len() != 2 {
684 Err(SgfErrorKind::ParseError.into())
685 } else {
686 let coords = input
687 .as_bytes()
688 .iter()
689 .map(|c| convert_u8_to_coordinate(*c))
690 .collect::<Vec<_>>();
691 Ok((coords[0], coords[1]))
692 }
693}
694
695#[inline]
698fn convert_u8_to_coordinate(c: u8) -> u8 {
699 if c > 96 {
700 c - 96
701 } else {
702 c - 38
703 }
704}