1#[doc = include_str!("../README.md")]
2mod builder;
3mod parser;
4
5use std::borrow::Cow;
6
7pub use builder::build;
8pub use builder::Builder;
9pub use parser::parse;
10
11use serde::{Deserialize, Serialize};
12
13use strum::EnumDiscriminants;
14use strum::EnumMessage;
15use thiserror::Error;
16
17#[derive(Error, Debug, Serialize, Deserialize, Clone, PartialEq)]
18pub enum SgfToolError {
19 #[error("Syntax issue")]
20 SyntaxIssue,
21
22 #[error("Root object not found")]
23 RootObjectNotFound,
24
25 #[error("Parse failed")]
26 ParseFailed,
27
28 #[error("Invalid number")]
29 InvalidNumber,
30
31 #[error("Invalid string")]
32 InvalidString,
33
34 #[error("Invalid float")]
35 InvalidFloat,
36
37 #[error("Player information not valid")]
38 PlayerInformationNotValid,
39
40 #[error("Point information not valid")]
41 PointInformationNotValid,
42
43 #[error("Node information not valid")]
44 NodeInformationNotValid,
45}
46
47#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
48pub struct Point<'a>(pub &'a str);
49
50impl Point<'_> {
51 pub fn xy(&self) -> (usize, usize) {
52 let position = self.0.to_lowercase();
53 let x = position.chars().nth(0).unwrap_or_default();
54 let y = position.chars().nth(1).unwrap_or_default();
55
56 (x as usize - 'a' as usize, y as usize - 'a' as usize)
57 }
58
59 pub fn xy_for_human(&self) -> (usize, usize) {
60 let (x, y) = self.xy();
61 (x + 1, y + 1)
62 }
63}
64
65#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
66pub enum Move<'a> {
67 Move(#[serde(borrow)] Point<'a>),
68 Pass,
69}
70
71#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
72pub struct PointRange<'a>(#[serde(borrow)] pub Point<'a>, pub Point<'a>);
73
74#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
75pub struct Figure<'a>(pub usize, pub &'a str);
76
77#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
78pub struct PointText<'a>(pub Point<'a>, pub &'a str);
79
80#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
81pub enum Player {
82 Black,
83 White,
84}
85
86#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
87pub struct Base<'a> {
88 #[serde(borrow)]
89 pub tokens: Vec<Cow<'a, Token<'a>>>,
90}
91
92impl<'a> Base<'a> {
93 pub fn add_token(&mut self, token: Token<'a>) {
94 self.tokens.push(Cow::Owned(token));
95 }
96
97 pub fn get(&self, token_type: TokenType) -> Option<&Cow<'a, Token<'a>>> {
98 self.tokens
99 .iter()
100 .find(|item| token_type == item.as_ref().into())
101 }
102
103 pub fn get_list(&self, token_type: TokenType) -> Vec<&Cow<'a, Token<'a>>> {
104 self.tokens
105 .iter()
106 .filter(|item| token_type == item.as_ref().into())
107 .collect::<Vec<_>>()
108 }
109}
110
111#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, EnumMessage, EnumDiscriminants)]
112#[strum_discriminants(name(TokenType))]
113pub enum Token<'a> {
114 Unknown(&'a str),
115
116 #[strum(message = "AP")]
118 Application(&'a str),
119
120 #[strum(message = "C")]
122 Comment(&'a str),
123
124 #[strum(message = "CP")]
126 Copyright(&'a str),
127
128 #[strum(message = "PB")]
130 BlackName(&'a str),
131
132 #[strum(message = "PW")]
134 WhiteName(&'a str),
135
136 #[strum(message = "BT")]
138 BlackTeam(&'a str),
139
140 #[strum(message = "WT")]
142 WhiteTeam(&'a str),
143
144 #[strum(message = "SZ")]
146 BoardSize(usize, usize),
147
148 Variation(Base<'a>),
150
151 #[strum(message = "FF")]
153 FileFormat(usize),
154
155 #[strum(message = "GM")]
157 GameType(usize),
158
159 #[strum(message = "CA")]
161 Charset(&'a str),
162
163 #[strum(message = "ST")]
165 VariationShown(usize),
166
167 #[strum(message = "PL")]
169 WhoseTurn(Player),
170
171 #[strum(message = "AB")]
173 BlackStones(Vec<Point<'a>>),
174
175 #[strum(message = "AW")]
177 WhiteStones(Vec<Point<'a>>),
178
179 #[strum(message = "B")]
181 BlackMove(Move<'a>),
182
183 #[strum(message = "W")]
185 WhiteMove(Move<'a>),
186
187 #[strum(message = "BR")]
189 BlackPlayerRank(&'a str),
190
191 #[strum(message = "WR")]
193 WhitePlayerRank(&'a str),
194
195 #[strum(message = "SO")]
197 Source(&'a str),
198
199 #[strum(message = "GN")]
201 GameName(&'a str),
202
203 #[strum(message = "N")]
205 NodeName(&'a str),
206
207 #[strum(message = "RU")]
209 Rule(&'a str),
210
211 #[strum(message = "KM")]
213 Komi(f32),
214
215 #[strum(message = "AN")]
217 PersonWhoProvidesAnnotations(&'a str),
218
219 #[strum(message = "AR")]
221 DrawArrow(PointRange<'a>),
222
223 #[strum(message = "CR")]
225 DrawCircle(Vec<Point<'a>>),
226
227 #[strum(message = "SQ")]
229 DrawSquare(Vec<Point<'a>>),
230
231 #[strum(message = "TR")]
233 DrawTriangle(Vec<Point<'a>>),
234
235 #[strum(message = "DD")]
237 GreyOut(Vec<Point<'a>>),
238
239 #[strum(message = "MA")]
241 MarkX(Vec<Point<'a>>),
242
243 #[strum(message = "HA")]
245 Handicap(usize),
246
247 #[strum(message = "RE")]
249 Result(&'a str),
250
251 #[strum(message = "FG")]
253 Figure(Option<Figure<'a>>),
254
255 #[strum(message = "PM")]
257 Printing(usize),
258
259 #[strum(message = "TM")]
261 TimeLimit(usize),
262
263 #[strum(message = "DT")]
265 Date(&'a str),
266
267 #[strum(message = "AV")]
269 Event(&'a str),
270
271 #[strum(message = "LB")]
273 PointText(Vec<PointText<'a>>),
274
275 #[strum(message = "RO")]
277 Round(&'a str),
278
279 #[strum(message = "US")]
281 SGFCreator(&'a str),
282
283 #[strum(message = "VW")]
285 ViewOnly(Vec<PointRange<'a>>),
286
287 #[strum(message = "MN")]
289 MoveNumber(usize),
290}
291
292#[cfg(test)]
296mod tests {
297 use crate::builder::Builder;
298 use crate::parser::parse;
299 use crate::*;
300
301 #[test]
302 fn basic_sgf_parse() -> Result<(), SgfToolError> {
303 let result = parse("()")?;
304 assert_eq!(result.tokens.len(), 0);
305 assert_eq!(parse("(a)"), Err(SgfToolError::SyntaxIssue));
306 assert_eq!(parse("(1)"), Err(SgfToolError::SyntaxIssue));
307 assert_eq!(parse("("), Err(SgfToolError::SyntaxIssue));
308 assert_eq!(parse(")"), Err(SgfToolError::SyntaxIssue));
309 assert_eq!(parse(""), Err(SgfToolError::SyntaxIssue));
310 assert_eq!(parse("-"), Err(SgfToolError::SyntaxIssue));
311 assert_eq!(parse(" "), Err(SgfToolError::SyntaxIssue));
312 Ok(())
313 }
314
315 #[test]
316 fn sgf_parse() -> Result<(), SgfToolError> {
317 let result = parse(
318 r#"(
319 ;FF[4]
320 C[root]
321 (
322 ;C[a]
323 ;C[b]
324 (
325 ;C[c]
326 )
327 (
328 ;C[d]
329 ;C[e]
330 )
331 )
332 (
333 ;C[f]
334 (
335 ;C[g]
336 ;C[h]
337 ;C[i]
338 )
339 (
340 ;C[j]
341 )
342 )
343)"#,
344 )?;
345 assert_eq!(result.tokens.len(), 4);
346 assert_eq!(result.tokens[0].as_ref(), &Token::FileFormat(4));
347 assert_eq!(result.tokens[1].as_ref(), &Token::Comment("root"));
348
349 if let Token::Variation(trees) = result.tokens[2].as_ref() {
350 assert_eq!(trees.tokens.len(), 4);
351 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("a"));
352 assert_eq!(trees.tokens[1].as_ref(), &Token::Comment("b"));
353
354 if let Token::Variation(trees) = trees.tokens[2].as_ref() {
355 assert_eq!(trees.tokens.len(), 1);
356 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("c"));
357 } else {
358 assert!(false, "Variation not found");
359 }
360
361 if let Token::Variation(trees) = trees.tokens[3].as_ref() {
362 assert_eq!(trees.tokens.len(), 2);
363 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("d"));
364 assert_eq!(trees.tokens[1].as_ref(), &Token::Comment("e"));
365 } else {
366 assert!(false, "Variation not found");
367 }
368 } else {
369 assert!(false, "Variation not found");
370 }
371
372 if let Token::Variation(trees) = &result.tokens[3].as_ref() {
373 assert_eq!(trees.tokens.len(), 3);
374 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("f"));
375
376 if let Token::Variation(trees) = trees.tokens[1].as_ref() {
377 assert_eq!(trees.tokens.len(), 3);
378 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("g"));
379 assert_eq!(trees.tokens[1].as_ref(), &Token::Comment("h"));
380 assert_eq!(trees.tokens[2].as_ref(), &Token::Comment("i"));
381 } else {
382 assert!(false, "Variation not found");
383 }
384
385 if let Token::Variation(trees) = trees.tokens[2].as_ref() {
386 assert_eq!(trees.tokens.len(), 1);
387 assert_eq!(trees.tokens[0].as_ref(), &Token::Comment("j"));
388 } else {
389 assert!(false, "Variation not found");
390 }
391 } else {
392 assert!(false, "Variation not found");
393 }
394
395 parse(
396 r#"
397 (;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[1]
398 PB[Takemiya Masaki]BR[9 dan]PW[Cho Chikun]
399 WR[9 dan]RE[W+Resign]KM[5.5]TM[28800]DT[1996-10-18,19]
400 EV[21st Meijin]RO[2 (final)]SO[Go World #78]US[Arno Hollosi]
401 ;B[pd];W[dp];B[pp];W[dd];B[pj];W[nc];B[oe];W[qc];B[pc];W[qd]
402 (;B[qf];W[rf];B[rg];W[re];B[qg];W[pb];B[ob];W[qb]
403 (;B[mp];W[fq];B[ci];W[cg];B[dl];W[cn];B[qo];W[ec];B[jp];W[jd]
404 ;B[ei];W[eg];B[kk]LB[qq:a][dj:b][ck:c][qp:d]N[Figure 1]
405
406 ;W[me]FG[257:Figure 2];B[kf];W[ke];B[lf];W[jf];B[jg]
407 (;W[mf];B[if];W[je];B[ig];W[mg];B[mj];W[mq];B[lq];W[nq]
408 (;B[lr];W[qq];B[pq];W[pr];B[rq];W[rr];B[rp];W[oq];B[mr];W[oo];B[mn]
409 (;W[nr];B[qp]LB[kd:a][kh:b]N[Figure 2]
410
411 ;W[pk]FG[257:Figure 3];B[pm];W[oj];B[ok];W[qr];B[os];W[ol];B[nk];W[qj]
412 ;B[pi];W[pl];B[qm];W[ns];B[sr];W[om];B[op];W[qi];B[oi]
413 (;W[rl];B[qh];W[rm];B[rn];W[ri];B[ql];W[qk];B[sm];W[sk];B[sh];W[og]
414 ;B[oh];W[np];B[no];W[mm];B[nn];W[lp];B[kp];W[lo];B[ln];W[ko];B[mo]
415 ;W[jo];B[km]N[Figure 3])
416
417 (;W[ql]VW[ja:ss]FG[257:Dia. 6]MN[1];B[rm];W[ph];B[oh];W[pg];B[og];W[pf]
418 ;B[qh];W[qe];B[sh];W[of];B[sj]TR[oe][pd][pc][ob]LB[pe:a][sg:b][si:c]
419 N[Diagram 6]))
420
421 (;W[no]VW[jj:ss]FG[257:Dia. 5]MN[1];B[pn]N[Diagram 5]))
422
423 (;B[pr]FG[257:Dia. 4]MN[1];W[kq];B[lp];W[lr];B[jq];W[jr];B[kp];W[kr];B[ir]
424 ;W[hr]LB[is:a][js:b][or:c]N[Diagram 4]))
425
426 (;W[if]FG[257:Dia. 3]MN[1];B[mf];W[ig];B[jh]LB[ki:a]N[Diagram 3]))
427
428 (;W[oc]VW[aa:sk]FG[257:Dia. 2]MN[1];B[md];W[mc];B[ld]N[Diagram 2]))
429
430 (;B[qe]VW[aa:sj]FG[257:Dia. 1]MN[1];W[re];B[qf];W[rf];B[qg];W[pb];B[ob]
431 ;W[qb]LB[rg:a]N[Diagram 1]))
432 "#,
433 )?;
434
435 parse(
436 r#"(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[2]
437 PB[Cho Chikun]BR[9 dan]PW[Ryu Shikun]WR[9 dan]RE[W+2.5]KM[5.5]
438 DT[1996-08]EV[51st Honinbo]RO[5 (final)]SO[Go World #78]US[Arno Hollosi]
439 ;B[qd];W[dd];B[fc];W[df];B[pp];W[dq];B[kc];W[cn];B[pj];W[jp];B[lq];W[oe]
440 ;B[pf];W[ke];B[id];W[lc];B[lb];W[kb];B[jb];W[kd];B[ka];W[jc];B[ic];W[kb]
441 ;B[mc];W[qc]N[Figure 1]
442
443 ;B[pd]FG[257:Figure 2];W[pc];B[od];W[oc];B[kc];W[nd];B[nc];W[kb];B[rd];W[pe]
444 (;B[rf];W[md];B[kc];W[qe];B[re];W[kb];B[mb];W[qf];B[qg];W[pg];B[qh];W[kc]
445 ;B[hb];W[nf];B[ch];W[cj];B[eh];W[ob]
446 (;B[cc];W[dc];B[db];W[bf];B[bb]
447 ;W[bh]LB[of:a][mf:b][rc:c][di:d][ja:e]N[Figure 2]
448
449 ;B[qp]FG[257:Figure 3];W[lo];B[ej];W[oq]
450 (;B[np];W[mq];B[mp];W[lp]
451 (;B[kq];W[nq];B[op];W[jq];B[mr];W[nr];B[lr];W[qr];B[jr];W[ir];B[hr];W[iq]
452 ;B[is];W[ks];B[js];W[gq];B[gr];W[fq];B[pq];W[pr];B[ns];W[or];B[rq];W[hq]
453 ;B[rr];W[cl];B[cg];W[bg];B[og];W[ng]
454 (;B[ci];W[bi];B[dj];W[dk];B[mm];W[gk];B[gi];W[mn];B[nm];W[kl];B[nh];W[mh]
455 ;B[mi];W[li];B[lh];W[mg];B[ek];W[el];B[ik]LB[kr:a]N[Figure 3]
456
457 ;W[ki]FG[257:Figure 4];B[fl];W[fk];B[gl];W[hk];B[hl];W[hj];B[jl];W[kk];B[km]
458 ;W[lm];B[ll];W[jm];B[jj];W[ji];B[kj];W[lj];B[ij];W[hi];B[em];W[dl];B[ii]
459 ;W[hh];B[ih];W[hg];B[ln];W[kn];B[lm];W[im];B[il];W[fg];B[lk];W[ni];B[ef]
460 ;W[eg];B[dg];W[ff];B[oh];W[of];B[oj];W[ph];B[oi];W[mj];B[ee];W[fe];B[de]
461 ;W[ed];B[ce];W[cf];B[rb];W[rc];B[sc];W[qb];B[sb];W[la];B[ma];W[na];B[ja]
462 ;W[nb];B[la];W[pa];B[be];W[fd];B[bj];W[ck];B[ec];W[hs];B[gs];W[fr];B[os]
463 ;W[ps];B[ms];W[nk];B[ok];W[kp];B[fo];W[fs];B[qq];W[hs];B[do];W[co];B[ig]
464 ;W[gc];B[gb];W[jf];B[di];W[fi];B[hf];W[gf];B[af];W[mo];B[he];W[kr];B[qs]
465 ;W[no];B[oo];W[nn];B[on];W[nl];B[ol];W[gn];B[fn];W[in];B[nj];W[mk];B[jg]
466 ;W[kg];B[mi];W[jh];B[ag];W[bk];B[ah];W[aj];B[fh];W[fj];B[gd];W[ra];B[dp]
467 ;W[cp];B[go];W[gm];B[fm];W[sd];B[se];W[ho];B[hm];W[hn];B[ep];W[eq];B[cd]
468 ;W[ei];B[dn];W[gp];B[pi];W[pf];B[dm];W[cm];B[je];W[jd];B[if];W[ie];B[ko]
469 ;W[jo];B[je];W[kf];B[ni];W[dh];B[ge];W[ie];B[rg];W[je]N[Figure 4])
470
471 (;B[dk]FG[257:Dia. 6]MN[1];W[ck];B[gk]N[Diagram 6]))
472
473 (;B[nq]VW[ai:ss]FG[257:Dia. 5]MN[1];W[mr];B[nr];W[lr]TR[oq]N[Diagram 5]))
474
475 (;B[mp]VW[ai:ss]FG[257:Dia. 4]MN[1];W[op];B[oo];W[no];B[mo];W[on];B[po]
476 ;W[mn];B[np];W[nn];B[or]N[Diagram 4]))
477
478 (;B[rc]VW[aa:sj]FG[257:Dia. 2]MN[1];W[rb];B[sb];W[la];B[ma];W[na];B[ja]
479 ;W[pa]N[Diagram 2])
480
481 (;B[rb]VW[aa:sj]FG[257:Dia. 3]MN[1];W[rc];B[sc];W[qb];B[pa];W[sb];B[sa]
482 ;W[sd];B[qa]N[Diagram 3]))
483
484 (;B[qf]VW[aa:sj]FG[257:Dia. 1]MN[1];W[mb];B[kc];W[qe];B[ne];W[kb];B[md]
485 ;W[la];B[nb];W[eb]LB[ob:a][na:b][rc:c][sd:d]N[Diagram 1]))
486 "#,
487 )?;
488
489 Ok(())
490 }
491
492 #[test]
493 fn basic_test_1() -> Result<(), SgfToolError> {
494 let source = "(;C[Black to play and win, Igo Hatsuyo-ron Problem 120];AB[ra][hb][lb][fc][lc][bd][ld][ce][de][fe][le][me][oe][pe][bf][mf][of][og][dh][oh][ph][qh][rh][sh][di][mi][ni][oi][pi][aj][fj][lj][ak][ek][lk][rk][sk][al][el][il][pl][ql][am][bm][em][qm][rm][dn][fn][mn][co][fo][ko][oo][bp][cp][ep][pp][sp][fq][pq][qq][sq][cr][nr][pr][bs][ns][os][ps];AW[qa][ib][jb][mb][rb][hc][qc][cd][jd][nd][od][ke][qe][re][df][ff][pf][bg][cg][dg][gg][hg][kg][lg][ng][ah][hi][ki][ri][si][bj][cj][jj][nj][oj][pj][qj][dk][jk][ok][dl][jl][rl][sl][hm][jm][sm][bn][cn][en][jn][on][rn][sn][qo][ro][so][ap][hp][kp][lp][mp][op][qp][eq][rq][ar][ir][mr][or][ms])";
495 let mut buffer = String::new();
496 let tree = parse(&source)?;
497 tree.build(&mut buffer)?;
498 assert_eq!(buffer, source);
499 Ok(())
500 }
501
502 #[test]
503 fn basic_test_2() -> Result<(), SgfToolError> {
504 let source = "(;AW[ca][cb][cc][bd][cd];AB[da][eb][dc][ec][dd][fd][be][ce][de];B[ab];W[bb];B[ac];W[ad];B[aa];C[RIGHT])";
505 let mut buffer = String::new();
506 let tree = parse(&source)?;
507 tree.build(&mut buffer)?;
508 assert_eq!(buffer, source);
509 Ok(())
510 }
511
512 #[test]
513 fn basic_test_3() -> Result<(), SgfToolError> {
514 let source = "(;AW[hh][lh][hi][ji][li][lj];AB[kg][lg][mg][mh][mi][mj][kk][lk][mk];C[Black to play and catch the three stones.](;B[ki];W[kh](;B[jh];W[kj];B[jj];W[ki];B[ii];C[RIGHT])(;B[kj];W[jh]))(;B[jh];W[jj])(;B[jj];W[jh])(;B[ii];W[jj]))";
515 let mut buffer = String::new();
516 let tree = parse(&source)?;
517 tree.build(&mut buffer)?;
518 assert_eq!(buffer, source);
519 Ok(())
520 }
521
522 #[test]
523 fn basic_test_4() -> Result<(), SgfToolError> {
524 let source = "(;FF[4];C[root];SZ[19];B[aa];W[ab];B[])";
525 let mut buffer = String::new();
526 let tree = parse(&source)?;
527 tree.build(&mut buffer)?;
528 assert_eq!(buffer, source);
529 Ok(())
530 }
531
532 #[test]
533 fn basic_test_5() -> Result<(), SgfToolError> {
534 let point = Point("ss");
535 assert_eq!(point.xy(), (18, 18));
536 assert_eq!(point.xy_for_human(), (19, 19));
537 Ok(())
538 }
539
540 #[test]
541 fn basic_test_6() -> Result<(), SgfToolError> {
542 let source = "(;FF[4];C[root];SZ[19];B[aa];W[ab];B[])";
543 let tree = parse(&source)?;
544
545 assert_eq!(
546 tree.get(TokenType::FileFormat),
547 Some(Cow::Owned(Token::FileFormat(4))).as_ref()
548 );
549 assert_eq!(
550 tree.get(TokenType::Comment),
551 Some(Cow::Owned(Token::Comment("root"))).as_ref()
552 );
553 assert_eq!(
554 tree.get(TokenType::BoardSize),
555 Some(Cow::Owned(Token::BoardSize(19, 19))).as_ref()
556 );
557 assert_eq!(
558 tree.get(TokenType::BlackMove),
559 Some(Cow::Owned(Token::BlackMove(Move::Move(Point("aa"))))).as_ref()
560 );
561
562 let items = tree.get_list(TokenType::BlackMove);
563 assert_eq!(items.len(), 2);
564 assert_eq!(
565 items.get(0),
566 Some(Cow::Owned(Token::BlackMove(Move::Move(Point("aa")))))
567 .as_ref()
568 .as_ref()
569 );
570 assert_eq!(
571 items.get(1),
572 Some(Cow::Owned(Token::BlackMove(Move::Pass)))
573 .as_ref()
574 .as_ref()
575 );
576 Ok(())
577 }
578}