429 lines
7.7 KiB
Go
429 lines
7.7 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type property struct {
|
||
|
kind string
|
||
|
value string
|
||
|
}
|
||
|
|
||
|
type node []property
|
||
|
|
||
|
type game struct {
|
||
|
bPlayer string
|
||
|
bRank string
|
||
|
bTeam string
|
||
|
commentor string
|
||
|
copyright string
|
||
|
date string
|
||
|
event string
|
||
|
eventRnd string
|
||
|
gameName string
|
||
|
genCom string
|
||
|
handicap string
|
||
|
komi string
|
||
|
place string
|
||
|
result string
|
||
|
rules string
|
||
|
size int
|
||
|
source string
|
||
|
time string
|
||
|
wPlayer string
|
||
|
wRank string
|
||
|
wTeam string
|
||
|
board [][]string
|
||
|
}
|
||
|
|
||
|
var black string = " X"
|
||
|
var white string = " O"
|
||
|
|
||
|
func (g *game) overview(result bool) string {
|
||
|
var out strings.Builder
|
||
|
out.WriteString("\n\n")
|
||
|
if g.gameName != "" {
|
||
|
out.WriteString(" Game: ")
|
||
|
out.WriteString(g.gameName)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.event != "" {
|
||
|
out.WriteString(" Event: ")
|
||
|
out.WriteString(g.event)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.eventRnd != "" {
|
||
|
out.WriteString(" Round: ")
|
||
|
out.WriteString(g.eventRnd)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.date != "" {
|
||
|
out.WriteString(" Date: ")
|
||
|
out.WriteString(g.date)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.place != "" {
|
||
|
out.WriteString(" Place: ")
|
||
|
out.WriteString(g.place)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.commentor != "" {
|
||
|
out.WriteString(" Commentor: ")
|
||
|
out.WriteString(g.commentor)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.source != "" {
|
||
|
out.WriteString(" Kifu Source: ")
|
||
|
out.WriteString(g.source)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.copyright != "" {
|
||
|
out.WriteString(" Copyright: ")
|
||
|
out.WriteString(g.copyright)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
|
||
|
out.WriteString("\n")
|
||
|
out.WriteString(" Black ( X ): ")
|
||
|
out.WriteString(g.bPlayer)
|
||
|
out.WriteString("\n")
|
||
|
out.WriteString(" White ( O ): ")
|
||
|
out.WriteString(g.wPlayer)
|
||
|
out.WriteString("\n")
|
||
|
if g.komi != "" {
|
||
|
out.WriteString(" Komi: ")
|
||
|
out.WriteString(g.komi)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.time != "" {
|
||
|
out.WriteString(" Time: ")
|
||
|
out.WriteString(g.time)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if g.rules != "" {
|
||
|
out.WriteString(" Rules: ")
|
||
|
out.WriteString(g.rules)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
if result {
|
||
|
out.WriteString("\n")
|
||
|
out.WriteString(" Result: ")
|
||
|
out.WriteString(g.result)
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
out.WriteString("\n")
|
||
|
|
||
|
return out.String()
|
||
|
}
|
||
|
|
||
|
func (g *game) writeboard() string {
|
||
|
var out strings.Builder
|
||
|
out.WriteString(" ")
|
||
|
for i, _ := range g.board {
|
||
|
out.WriteString(" ")
|
||
|
out.WriteRune(rune(i + 65))
|
||
|
}
|
||
|
out.WriteString("\n")
|
||
|
|
||
|
for i, row := range g.board {
|
||
|
out.WriteString(fmt.Sprintf("%2d", i + 1))
|
||
|
for _, col := range row {
|
||
|
out.WriteString(col)
|
||
|
}
|
||
|
out.WriteString("\n")
|
||
|
}
|
||
|
return out.String()
|
||
|
}
|
||
|
|
||
|
|
||
|
// Parse the SGF into a slice of nodes
|
||
|
|
||
|
func generateAST(path string) []node {
|
||
|
file, err := os.Open(path)
|
||
|
defer file.Close()
|
||
|
if err != nil {
|
||
|
fmt.Println(err.Error())
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
bytes, err := ioutil.ReadAll(file)
|
||
|
if err != nil {
|
||
|
fmt.Println(err.Error())
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
ast, err := parseInputFile(string(bytes))
|
||
|
if err != nil {
|
||
|
fmt.Println(err.Error())
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
return ast
|
||
|
}
|
||
|
|
||
|
func parseInputFile(sgf string) ([]node, error) {
|
||
|
seq := make([]node, 0, 10)
|
||
|
current := 0
|
||
|
|
||
|
// get to start of game
|
||
|
for sgf[current] != '(' {
|
||
|
current++
|
||
|
}
|
||
|
current++
|
||
|
|
||
|
for ;; current++ {
|
||
|
if current >= len(sgf) {
|
||
|
return seq, fmt.Errorf("Invalid kifu: no end to game tree before EOF")
|
||
|
} else if sgf[current] == '(' {
|
||
|
err := eatSubTree(&sgf, ¤t)
|
||
|
if err != nil {
|
||
|
return seq, err
|
||
|
}
|
||
|
} else if sgf[current] == ')' {
|
||
|
break
|
||
|
} else if sgf[current] == ';' {
|
||
|
current++
|
||
|
n, err := parseNode(&sgf, ¤t)
|
||
|
if err == nil {
|
||
|
seq = append(seq, n)
|
||
|
}
|
||
|
} else {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return seq, nil
|
||
|
}
|
||
|
|
||
|
func validateKind(k string) bool {
|
||
|
switch k {
|
||
|
case "B", "C", "W", "AB", "AN", "AW",
|
||
|
"BR", "BT", "CA", "CP", "DT", "EV",
|
||
|
"GC", "GM", "GN", "HA", "KM",
|
||
|
"PB", "PL", "PW", "RE", "RO", "RU",
|
||
|
"SO", "SZ", "TM", "WR", "WT":
|
||
|
return true
|
||
|
default:
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func parseProperty(sgf *string, cur *int) (property, error) {
|
||
|
var prop = property{}
|
||
|
var kindBuild strings.Builder
|
||
|
var valBuild strings.Builder
|
||
|
for {
|
||
|
if (*sgf)[*cur] >= 'A' && (*sgf)[*cur] <= 'Z' {
|
||
|
kindBuild.WriteByte((*sgf)[*cur])
|
||
|
} else if (*sgf)[*cur] == '[' {
|
||
|
break
|
||
|
}
|
||
|
*cur++
|
||
|
}
|
||
|
|
||
|
k := kindBuild.String()
|
||
|
if !validateKind(k) {
|
||
|
for (*sgf)[*cur] != ']' {
|
||
|
*cur++
|
||
|
continue
|
||
|
}
|
||
|
return property{}, fmt.Errorf("Ignore prop")
|
||
|
}
|
||
|
prop.kind = k
|
||
|
|
||
|
if (*sgf)[*cur] == '[' {
|
||
|
*cur++
|
||
|
} else {
|
||
|
for (*sgf)[*cur] != '[' {
|
||
|
*cur++
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
for (*sgf)[*cur] != ']' {
|
||
|
valBuild.WriteByte((*sgf)[*cur])
|
||
|
*cur++
|
||
|
}
|
||
|
prop.value = valBuild.String()
|
||
|
*cur++
|
||
|
|
||
|
return prop, nil
|
||
|
}
|
||
|
|
||
|
func parseNode(sgf *string, cur *int) (node, error) {
|
||
|
// TODO finish node parsing
|
||
|
n := make(node, 0, 1)
|
||
|
for (*sgf)[*cur] != ';' {
|
||
|
if (*sgf)[*cur] == ')' {
|
||
|
break
|
||
|
}
|
||
|
prop, err := parseProperty(sgf, cur)
|
||
|
if err == nil {
|
||
|
n = append(n, prop)
|
||
|
} else {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
if len(n) == 0 {
|
||
|
return n, fmt.Errorf("Bad node. Do not add.")
|
||
|
}
|
||
|
*cur--
|
||
|
return n, nil
|
||
|
}
|
||
|
|
||
|
|
||
|
func eatSubTree(sgf *string, cur *int) error {
|
||
|
for (*sgf)[*cur] != ')' {
|
||
|
*cur++
|
||
|
if *cur >= len(*sgf) {
|
||
|
return fmt.Errorf("Invalid kifu: no end to sub tree befor EOF")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Generate a game struct
|
||
|
|
||
|
func makeGame() game {
|
||
|
return game{
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
19,
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
"",
|
||
|
[][]string{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func gameStruct(a []node) game {
|
||
|
if len(a) < 1 {
|
||
|
fmt.Println("Invalid kifu: No game metadata found")
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
game := makeGame()
|
||
|
adds := make([]property,0,1)
|
||
|
for _, prop := range a[0] {
|
||
|
switch prop.kind {
|
||
|
case "SZ":
|
||
|
sz, err := strconv.Atoi(prop.value)
|
||
|
if err != nil {
|
||
|
fmt.Println("Invalid board size supplied")
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
game.size = sz
|
||
|
game.board = make([][]string, sz, sz)
|
||
|
for ri, _ := range game.board {
|
||
|
cols := make([]string, sz, sz)
|
||
|
for ci, _ := range cols {
|
||
|
cols[ci] = " ."
|
||
|
}
|
||
|
game.board[ri] = cols
|
||
|
}
|
||
|
case "GC":
|
||
|
game.genCom = prop.value
|
||
|
case "HA":
|
||
|
game.handicap = prop.value
|
||
|
case "KM":
|
||
|
game.komi = prop.value
|
||
|
case "TM":
|
||
|
game.time = prop.value
|
||
|
case "BR":
|
||
|
game.bRank = prop.value
|
||
|
case "BT":
|
||
|
game.bTeam = prop.value
|
||
|
case "WR":
|
||
|
game.wRank = prop.value
|
||
|
case "WT":
|
||
|
game.wTeam = prop.value
|
||
|
case "CP":
|
||
|
game.copyright = prop.value
|
||
|
case "PL":
|
||
|
game.place = prop.value
|
||
|
case "PB":
|
||
|
game.bPlayer = prop.value
|
||
|
case "PW":
|
||
|
game.wPlayer = prop.value
|
||
|
case "RE":
|
||
|
game.result = prop.value
|
||
|
case "RO":
|
||
|
game.eventRnd = prop.value
|
||
|
case "RU":
|
||
|
game.rules = prop.value
|
||
|
case "SO":
|
||
|
game.source = prop.value
|
||
|
case "EV":
|
||
|
game.event = prop.value
|
||
|
case "DT":
|
||
|
game.date = prop.value
|
||
|
case "GN":
|
||
|
game.gameName = prop.value
|
||
|
case "GM":
|
||
|
if prop.value != "1" {
|
||
|
fmt.Println("This is not a Go kifu, it is for a different game")
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
case "AN":
|
||
|
game.commentor = prop.value
|
||
|
case "AB", "AW":
|
||
|
adds = append(adds, prop)
|
||
|
}
|
||
|
}
|
||
|
for _, p := range adds {
|
||
|
var piece string
|
||
|
if p.kind == "AB" {
|
||
|
piece = black
|
||
|
} else {
|
||
|
piece = white
|
||
|
}
|
||
|
r, c := getCoords(p.value, game.size)
|
||
|
game.board[r][c] = piece
|
||
|
}
|
||
|
return game
|
||
|
}
|
||
|
|
||
|
func getCoords(co string, boardSize int) (int, int) {
|
||
|
if len([]rune(co)) != 2 {
|
||
|
fmt.Printf("Invalid ADD or MOVE property: %s\n", co)
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
var row, col int
|
||
|
co = strings.ToUpper(co)
|
||
|
col = int(co[0]) - 65
|
||
|
row = int(co[1]) - 65
|
||
|
if col < 0 || col > boardSize - 1 || row < 0 || row > boardSize - 1 {
|
||
|
fmt.Println("Invalid move coordinates found")
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
return row, col
|
||
|
}
|
||
|
|
||
|
|
||
|
func main() {
|
||
|
if len(os.Args) != 2 {
|
||
|
fmt.Println("Must provide path to input sgf")
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
ast := generateAST(os.Args[1])
|
||
|
game := gameStruct(ast)
|
||
|
overview := game.overview(false)
|
||
|
fmt.Print(overview)
|
||
|
fmt.Print(game.writeboard())
|
||
|
}
|