sgf2gopher/main.go

518 lines
10 KiB
Go

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())
}