C#编辑解析sgf开源项目sgfeditor

C#版SGF编辑器,可读取和写入.sgf文件,编辑游戏树。

更多:开源围棋

链接: https://pan.baidu.com/s/1v0VDqjQOyBvjLIJrZbUn4w 提取码: sd4s
GitHub
https://github.com/billchiles/sgfeditor

包含C#写的围棋GUI界面。
还有python版本的sgf编辑

//// sgfparser.cs parses .sgf files.  Each parse takes a file and creates a list
//// of ParsedNodes.  The list of nodes may not adhere to normal game moves such
//// as alternating colors, or starting with B in an even game and W with
//// handicaps.  The first node is the root node and should be game properties
//// while following nodes should represent a game, but the nodes could
//// represent setup for a problem.
////

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;


namespace SgfEd {

    public class ParsedGame {

        public ParsedNode Nodes { get; set; }

        public ParsedGame () {
            // nodes is the only public member.
            this.Nodes = null;
        }

        //// __str__ produces a strong that when printed to a file generates a valid
        //// .sgf file.
        ////
        public override string ToString () {
            if (this.Nodes == null)
                return "";  // Min tree is "(;)", but that implies one empty node
            else
                return "(" + this.NodesString(this.Nodes) + ")";
        }
    
        //// _nodes_string returns a string for a series of nodes, and the caller
        //// needs to supply the open and close parens that bracket the series.
        ////
        private string NodesString (ParsedNode nodes) {
            var res = "";
            while (nodes.Next != null) {
                // Get one node's string with a leading newline if it is not the
                // first.
                res += nodes.NodeString(res != "");
                if (nodes.Branches != null) {
                    foreach (var n in nodes.Branches)
                        res = res + "\n(" + this.NodesString(n) + ")";
                    return res;
                }
                nodes = nodes.Next;
            }
            res += nodes.NodeString(res != ""); // Test res, could be single node branch.
            return res;
        }

    } // ParsedGame class



    public class ParsedNode {

        public ParsedNode Next;
        public ParsedNode Previous;
        public List<ParsedNode> Branches;
        public Dictionary<string, List<string>> Properties;

        public ParsedNode() {
            this.Next = null;
            this.Previous = null;
            this.Branches = null;
            this.Properties = new Dictionary<string, List<string>>();
        }

        //// node_str returns the string for one node, taking a flag for a
        //// preceding newline and the dictionary of properties for the node.
        //// Game uses this for error reporting.
        ////
        public string NodeString(bool newline) {
            var props = this.Properties;
            string s;
            if (newline)
                s = "\n;";
            else
                s = ";";
            // Print move property first for readability of .sgf file by humans.
            if (props.ContainsKey("B"))
                s = s + "B" + this.EscapePropertyValues("B", props["B"]);
            if (props.ContainsKey("W"))
                s = s + "W" + this.EscapePropertyValues("W", props["W"]);
            foreach (var kv in props) {
                var k = kv.Key;
                if (k == "B" || k == "W") continue;
                s = s + k + this.EscapePropertyValues(k, kv.Value);
            }
            return s;
        }

        //// _escaped_property_values returns a node's property value with escapes so that the .sgf
        //// is valid.  So, ] and \ must be preceded by a backslash.
        ////
        private string EscapePropertyValues(string id, List<string> values) {
            var res = "";
            foreach (var v in values) {
                res = res + "[";
                if (v.Contains(']') || v.Contains('\\')) {
                    var sb = new StringBuilder();
                    foreach (var c in v) {
                        if (c == ']' || c == '\\')
                            sb.Append('\\');
                        sb.Append(c);
                    }
                    res = res + sb.ToString() + "]";
                }
                else
                    res = res + v + "]";
            } //foreach
            return res;
        }

    } // ParsedNode class



    //// ParserAux provides stateless helpers for parsing.  Public members are used throughout
    //// the program.
    ////
    public class ParserAux {

        public static ParsedGame ParseFile (string name) {
            var l = new Lexer(File.ReadAllText(name));
            l.ScanFor("(", "Can't find game start");
            var g = new ParsedGame();
            g.Nodes = ParseNodes(l);
            return g;
        }

        //// _parse_nodes returns a linked list of ParseNodes.  It starts scanning for a
        //// semi-colon for the start of the first node.  If it encounters an open
        //// paren, it recurses and creates branches that follow the current node,
        //// making the next pointer of the current node point to the first node in the
        //// first branch.
        ////
        private static ParsedNode ParseNodes (Lexer lexer) {
            lexer.ScanFor(";", "Must be one node in each branch");
            var cur_node = ParseNode(lexer);
            var first = cur_node;
            var branching_yet = false;
            while (lexer.HasData()) {
                // Semi-colon starts another node, open paren starts a branch, close
                // paren stops list of nodes.  Scanning raises an exception if one of
                // these chars fails to follow (ignoring whitespace).
                var chr = lexer.ScanFor(";()");
                if (chr == ';') {
                    if (branching_yet)
                        throw new Exception("Found node after branching started.");
                    cur_node.Next = ParseNode(lexer);
                    cur_node.Next.Previous = cur_node;
                    cur_node = cur_node.Next;
                }
                else if (chr == '(') {
                    if (! branching_yet) {
                        cur_node.Next = ParseNodes(lexer);
                        cur_node.Next.Previous = cur_node;
                        cur_node.Branches = new List<ParsedNode>() { cur_node.Next };
                        branching_yet = true;
                    }
                    else {
                        var n = ParseNodes(lexer);
                        n.Previous = cur_node;
                        cur_node.Branches.Add(n);
                    }
                }
                else if (chr == ')')
                    return first;
                else
                    throw new FileFormatException("SGF file is malformed at char " + lexer.Location.ToString());
            }
            throw new FileFormatException("Unexpectedly hit EOF!");
        } // ParseNodes

        //// _parse_node returns a ParseNode with its properties filled in.
        ////
        private static ParsedNode ParseNode (Lexer lexer) {
            var node = new ParsedNode();
            // Loop properties ...
            while (lexer.HasData()) {
                var id = lexer.GetPropertyId();
                if (id == null)
                    // Expected to return from here due to no properties or syntax at end of properties.
                    return node;
                if (node.Properties.ContainsKey(id))
                    throw new Exception(string.Format("Encountered ID, {0}, twice for node -- file location {1}.",
                                        id, lexer.Location));
                lexer.ScanFor("[", "Expected property value");
                var i = -1;
                var values = new List<string>();
                node.Properties[id] = values;
                // Loop values for one property
                while (lexer.HasData()) {
                    // C and GC properties allow newline sequences in value.
                    values.Add(lexer.GetPropertyValue(id == "C" || id == "GC"));
                    i = lexer.PeekFor("[").Item1;
                    if (i == -1) break; // no new values
                    lexer.Location = i;
                }
            }
            throw new FileFormatException("Unexpectedly hit EOF!");
        } // ParseNode

    } // ParserAux Class



    //// The Lexer class provides stateless helpers for Parser or ParserAux.
    ////
    internal class Lexer {

        private string _data;
        private int _data_len;
        private int _index;

        internal Lexer (string contents) {
            this._data = contents;
            this._data_len = contents.Length;
            this._index = 0;
        }
    
        
        internal int Location { get {return this._index;} 
                                set {this._index = value;} }


        //// scan_for scans for any char in chars following whitespace.  If
        //// non-whitespace intervenes, this is an error.  Scan_for leaves _index
        //// after char and returns found char.
        ////
        internal char ScanFor(string chars, string errmsg = null) {
            var tmp = this.PeekFor(chars);
            var i = tmp.Item1;
            var c = tmp.Item2;
            if (i == -1) {
                if (errmsg != null)
                    errmsg = errmsg + " -- file location " + this._index.ToString();
                throw new Exception(errmsg ?? string.Format("Expecting one of '{0}' while scanning -- file location {1}",
                                                            chars, this._index));
            }
            else {
                this._index = i;
                return c;
            }
        }

        //// peek_for scans for any char in chars following whitespace.  If
        //// non-whitespace intervenes, this is an error.  Scan_for leaves _index
        //// unmodified.
        ////
        internal Tuple<int, char> PeekFor(string chars) { 
            var i = this._index;
            while (this.HasData()) {
                var c = this._data[i];
                i += 1;
                if (" \t\n\r\f\v".Contains(c))
                    continue;
                else if (chars.Contains(c))
                    return new Tuple<int, char>(i, c);
                else
                    return new Tuple<int, char>(-1, ((char)0));
            }
            return new Tuple<int, char>(-1, ((char)0));
        }
        

        internal bool HasData() {
            return this._index < this._data_len;
        }
    

        private Regex propertyIdRegexp = new Regex(@"\s*([A-Za-z]+)");

        //// get_property_id skips any whitespace and expects to find alphabetic chars
        //// that form a property name.  .NET does not match at start location by default.
        //// .NET returns extra groups, and naming the one group in the pattern does not help find group.
        //// This empirically assumes groups[1] is the group we want, no documentation on group[0] in .NET,
        //// but it appears to be the whole match, which lets us test if we matched at the start or not.
        ////
        internal string GetPropertyId () {
            var match = this.propertyIdRegexp.Match(this._data, this._index);
            if (match.Success && match.Groups[0].Index == this._index) {
                this._index += match.Length;
                return match.Groups[1].ToString();
            }
            return null;
        }

        //// get_property_id skips any whitespace and expects to find alphabetic chars
        //// that form a property name.  This function open codes a simple regexp that
        //// .NET matches incorrectly.  .NET fails to match at the start index provided
        //// in two ways.  It finds any match anywhere in the string, which is arguably
        //// appropriate, but when you use the match start syntax and provide a start
        //// index for searching, .NET always fails the match.
        ////
        //internal string GetPropertyId () {
        //    var i = this._index;
        //    // Skip any whitespace
        //    while (i < this._data_len) {
        //        var c = this._data[i];
        //        if (" \t\n\r\f\v".Contains(c)) {
        //            i += 1;
        //            continue;
        //        }
        //        else
        //            break;
        //    }
        //    // Grab alphabetic chars or fail
        //    var sb = new StringBuilder();
        //    while (i < this._data_len) {
        //        var c = this._data[i];
        //        if (('a' <= c && 'z' >= 'c') || ('A' <= c && 'Z' >= c)) {
        //            i += 1;
        //            sb.Append(c);
        //        }
        //        else
        //            break;
        //    }
        //    if (sb.Length > 0) {
        //        this._index = i;
        //        return sb.ToString();
        //    }
        //    else
        //        return null;
        //}

        //// get_property_value takes a flag as to whether un-escaped newlines get
        //// mapped to space or kept as-is.  It gobbles all the characters after a
        //// '[' (which has already been consumed) up to the next ']' and returns
        //// them as a string.  Keep_newlines distinguishes properties like C and GC
        //// that can have newlines in their values, but otherwise, newlines are
        //// assumed to be purely line-length management in the .sgf file.
        ////
        //// SGF "text" properties can have newlines, newlines following \ are removed
        //// along with the \, other escaped chars are kept verbatim except whitespace
        //// is converted to space.
        ////
        //// SGF "simpletext" properties are the same as "text" but have no newlines.
        ////
        internal string GetPropertyValue(bool keep_newlines) { 
            var res = new StringBuilder();
            while (this.HasData()) {
                var c = this._data[this._index];
                this._index += 1;
                if (((int)c) < ((int)' ')) { //if < space
                    // Map whitespace to spaces.
                    var tmp = this.CheckPropertyNewline(c);
                    var newline = tmp.Item1;
                    var c2 = tmp.Item2;
                    if (newline)
                        // Only map newline sequences according to keep_newlines.
                        if (keep_newlines) {
                            // Canonicalize newlines because 1) would write mixed newline sequences in different places   
                            // in .sgf depending on comments vs. other syntax, and 2) winRT textbox converts all line   
                            // endings with no option to preserve them.  Not needed for WPF, but doesn't hurt.
                            res.Append('\r');   
                            res.Append('\n');  
                        }
                        else
                            res.Append(" "); //convert newline to space to separate words
                    else
                        res.Append(" "); //convert whatever whitespace it was to space
                }
                else if (c == '\\') {
                    // Backslash quotes chars and erases newlines.
                    c = this._data[this._index];
                    this._index += 1;
                    var newline = this.CheckPropertyNewline(c).Item1;
                    if (! newline)
                        res.Append(c);
                }
                else if (c == ']')
                    return res.ToString();
                else
                    res.Append(c);
            }
            throw new FileFormatException("Unexpectedly hit EOF!");
        } // GetPropertyValue

        //// _check_property_newline check if c is part of a newline sequence.  If
        //// it is, then see if there's a second newline sequence char and gobble
        //// it.  Returns whether there was a newline sequence and what the second
        //// char was if it was part of the newline sequence.
        ////
        private Tuple<bool, char> CheckPropertyNewline (char c) {
            if (c == '\n' || c == '\r') {
                // Only map newline sequences according to keep_newlines.
                var c2 = this._data[this._index];
                if (c2 == '\n' || c2 == '\r') {
                    this._index += 1;
                    return new Tuple<bool, char>(true, c2);
                }
                return new Tuple<bool, char>(true, ((char)0));
            }
            else
                return new Tuple<bool, char>(false, ((char)0));
        }

    } // Lexer class

} // namespace

作者:

喜欢围棋和编程。

 
发布于 分类 编程标签

发表评论

电子邮件地址不会被公开。