package main import ( "bufio" "flag" "fmt" "io/ioutil" "os" "path/filepath" "strconv" "strings" ) type property struct { kind string value string } type node []property type game struct { bPlayer string bRank string bTeam string comment 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" var empty string = " ." var check string = "--" var host string var folder string var port string var out string 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 - - -\n\n") if g.bPlayer != "" { out.WriteString(" Black ( X ): ") out.WriteString(g.bPlayer) if g.bRank != "" { out.WriteString(" - ") out.WriteString(g.bRank) } out.WriteString("\n") } out.WriteString("\n") if g.wPlayer != "" { out.WriteString(" White ( O ): ") out.WriteString(g.wPlayer) if g.wRank != "" { out.WriteString(" - ") out.WriteString(g.wRank) } out.WriteString("\n") } 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") } out.WriteString("\n") return out.String() } func (g *game) handleNode(n node, num, max int) { g.comment = "" moved := false for _, p := range n { switch p.kind { case "C": g.comment = p.value case "B", "W": if moved { panic("Encountered two moves in one node") } moved = true if p.kind == "B" { g.move(p.value, black) } else { g.move(p.value, white) } } } if !moved && num > 0 { return } fileName := fmt.Sprintf("move%d.map", num) if num == 0 { fileName = "gophermap" } outputPath := filepath.Join(out, fileName) f, err := os.Create(outputPath) if err != nil { panic("Couldnt create file") } defer f.Close() w := bufio.NewWriter(f) if num == max { w.WriteString(g.overview(true)) } else { w.WriteString(g.overview(false)) } w.WriteString(g.writeboard()) w.WriteString(g.comment) w.WriteString("\n\n") gopherpath := filepath.Join(folder, "move") if num > 1 { w.WriteString(fmt.Sprintf("1 PREV Move\t%s%d.map\t%s\t%s\n", gopherpath, num - 1, host, port)) } else if num == 1 { firstfile := filepath.Join(folder, "gophermap") w.WriteString(fmt.Sprintf("1 PREV Move\t%s\t%s\t%s\n", firstfile, host, port)) } if num < max { w.WriteString(fmt.Sprintf("1 NEXT Move\t%s%d.map\t%s\t%s\n", gopherpath, num + 1, host, port)) } if num-5 >= 0 { w.WriteString(fmt.Sprintf("1 Jump BACK\t%s%d.map\t%s\t%s\n", gopherpath, num - 5, host, port)) } else if num > 0 { w.WriteString(fmt.Sprintf("1 Jump BACK\t%s%d.map\t%s\t%s\n", gopherpath, 0, host, port)) } if num+5 <= max { w.WriteString(fmt.Sprintf("1 Jump AHEAD\t%s%d.map\t%s\t%s\n", gopherpath, num + 5, host, port)) } else if num < max { w.WriteString(fmt.Sprintf("1 Jump AHEAD\t%s%d.map\t%s\t%s\n", gopherpath, max, host, port)) } w.WriteString("\n\n") w.Flush() } func (g *game) updateBoard(row, col int, match string) { var val int if row - 1 >= 0 && g.board[row - 1][col] == match { val = g.flood(row-1,col,match,check) if val > 0 { g.flood(row-1,col,check,match) } else { g.flood(row-1,col,check,empty) } } if row + 1 < g.size && g.board[row + 1][col] == match { val = g.flood(row+1,col,match,check) if val > 0 { g.flood(row+1,col,check,match) } else { g.flood(row+1,col,check,empty) } } if col - 1 >= 0 && g.board[row][col - 1] == match { val = g.flood(row,col-1,match,check) if val > 0 { g.flood(row,col-1,check,match) } else { g.flood(row,col-1,check,empty) } } if col + 1 < g.size && g.board[row][col + 1] == match { val = g.flood(row,col+1,match,check) if val > 0 { g.flood(row,col+1,check,match) } else { g.flood(row,col+1,check,empty) } } } func (g *game) flood(row, col int, match, update string) int { if g.board[row][col] != match { if g.board[row][col] == empty { return 1 } return 0 } g.board[row][col] = update var total int if row - 1 >= 0 { total += g.flood(row - 1, col, match, update) } if row + 1 < g.size { total += g.flood(row + 1, col, match, update) } if col - 1 >= 0 { total += g.flood(row, col - 1, match, update) } if col + 1 < g.size { total += g.flood(row, col + 1, match, update) } return total } func (g *game) move(move, piece string) { row, col := parseMove(move) g.board[row][col] = piece if piece == black { g.updateBoard(row, col, white) } else { g.updateBoard(row, col, black) } } func parseMove(m string) (int, int) { if len(m) != 2 { fmt.Printf("Invalid move: %s", m) os.Exit(1) } m = strings.ToUpper(m) return int(m[0])-65, int(m[1])-65 } // 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 := generateTree([]rune(string(bytes))) if err != nil { fmt.Println(err.Error()) os.Exit(1) } return ast } 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 } } // 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() { flag.StringVar(&host, "host", "colorfield.space", "The gopher host") flag.StringVar(&port, "port", "70", "Gopher port being used") flag.StringVar(&folder, "folder", "/", "Gopher folder") flag.StringVar(&out, "out", "./", "Local folder to generate files") flag.Parse() if len(flag.Args()) < 1 { fmt.Println("Must provide path to input sgf") os.Exit(1) } ast := generateAST(flag.Args()[0]) fmt.Println(ast) game := gameStruct(ast) numMoves := len(ast) - 1 if numMoves > 0 { for i, n := range ast { game.handleNode(n, i, numMoves) } } overview := game.overview(false) fmt.Print(overview) fmt.Print(game.writeboard()) }