From 4f5227cd8261fb816f92286244d882c7662db466 Mon Sep 17 00:00:00 2001
From: Anna Wiggins <annabunches@gmail.com>
Date: Sun, 15 Apr 2012 16:54:50 -0400
Subject: [PATCH] Added basic implementation to gtpsocket, started to sketch
 out networkthread

---
 lib/gtpsocket.py     | 108 +++++++++++++++++++++++++++++++++++++++----
 lib/networkthread.py |  33 +++++++++++++
 pygo.py              |   1 +
 3 files changed, 134 insertions(+), 8 deletions(-)
 create mode 100644 lib/networkthread.py

diff --git a/lib/gtpsocket.py b/lib/gtpsocket.py
index 01fd8cf..0b8c753 100644
--- a/lib/gtpsocket.py
+++ b/lib/gtpsocket.py
@@ -1,29 +1,80 @@
 # A class for communicating in Go Text Protocol
 #
 # An already-connected socket should be passed in, then this class
-# can be used to handle GTP simply
+# can be used to handle some aspects of GTP simply
+
+# Commands that any user of this class *must* support...
+# quit
+# boardsize
+# clear_board
+# komi
+# play
+# genmove
+
 
 class GTPSocket:
+    # By default the list only has commands we can handle internally in this class.
+    # Anything that uses this class should append any commands it can handle to this
+    # list. A common pattern would be:
+    #
+    # dispatcher = {'command1': function_1, 'command_2': function_2, 'command_3': function_3}
+    # gtp_socket = GTPSocket(socket)
+    # GTPSocket.known_cmds.extend(dispatcher.keys())
+    # gtp = gtp_socket.get()
+    # if gtp.type == 'command':
+    #     dispatcher[gtp.command](gtp)
+    known_cmds = ['protocol_version', 'name', 'version', 'known_command', 'list_commands']
+
     def __init__(self, socket):
         self.socket = None
-        self.id = None
+        self.last_id = None
 
-    def get_gtp(self):
+    def get(self):
         msg = None
 
         while msg is None:
             try:
-                msg = self.socket.recv(1024)
-                if not self._validate_gtp(msg):
+                msg = self.socket.recv(4096)
+                if msg[0] == '?':
+                    print "Error: GTP response: " + msg
+                    return self._msg_to_response(msg)
+                elif msg[0] == '=':
+                    return self._msg_to_response(msg)
+                elif not _validate_gtp(msg):
                     print 'Error: Incoming data was not a valid GTP message'
                     msg = None
             except:
                 pass
 
+        gtp = self._msg_to_gtp(msg)
 
-    def send_gtp(self, msg):
+        # Some gtp commands should be handled internally
+        if gtp.command == 'protocol_version':
+            self.send_response('2', gtp.id)
+
+        elif gtp.command == 'name':
+            self.send_response('pygo', gtp.id)
+
+        elif gtp.command == 'version':
+            self.send_response('', gtp.id)
+
+        elif gtp.command == 'known_command':
+            if gtp.arguments[0] in GTPSocket.known_cmds:
+                resp = 'true'
+            else:
+                resp = 'false'
+            self.send_response(resp, gtp.id)
+
+        elif gtp.command == 'list_commands':
+            self.send_response(''.join(GTPSocket.known_cmds, '\n'), gtp.id)
+
+
+
+    def send(self, msg):
         try:
+            msg = str(self.last_id + 1) + ' ' + msg + "\n"
             if _validate_gtp(msg):
+                self.last_id += 1
                 self.socket.send(msg)
                 return True
             else:
@@ -33,5 +84,46 @@ class GTPSocket:
             return False
 
 
-    def _validate_gtp(self, gtp):
-        return True
+    def send_response(self, msg, resp_id=None, is_error=False):
+        try:
+            to_send = ''
+            if is_error:
+                to_send += '?'
+            else:
+                to_send += '='
+
+            if resp_id is not None:
+                to_send += str(resp_id)
+
+            to_send += msg + "\n\n"
+            self.socket.send(to_send)
+            return True
+
+        except:
+            return False
+        
+
+    def _msg_to_gtp(self, msg):
+        gtp = Object()
+        data = msg.split(' ')
+        gtp.type = 'message'
+        gtp.id = int(data[0])
+        gtp.command = data[1]
+
+        if len(data) > 2:
+            gtp.arguments = data[2:]
+
+        return gtp
+
+
+    def _msg_to_response(self, msg):
+        resp = Object()
+        resp.type = 'response'
+        
+        
+
+# For now, we *require* our messages to have an id.
+# This violates the protocol, but we'll deal with the
+# more complicated case later.
+def _validate_gtp(msg):
+    return re.match(r'\d+?\s+\w+?', msg)
diff --git a/lib/networkthread.py b/lib/networkthread.py
new file mode 100644
index 0000000..2fd691f
--- /dev/null
+++ b/lib/networkthread.py
@@ -0,0 +1,33 @@
+# This class implements a thread that handles the networking connection.
+# It expects a copy of the board and an open socket to the remote host, which
+# it will bind to a GTPSocket.
+# This is the only thread that should be calling get(), but send() could be called
+# twice, so we wrap it in a mutex.
+# The board is also wrapped in a mutex, even though only this thread should modify it
+# if it exists.
+
+import threading
+
+
+class NetworkThread(threading.Thread):
+    dispatcher = {
+        'quit': None,
+        'boardsize': None,
+        'clear_board': None,
+        'komi': None,
+        'play': None,
+        'genmove': None
+        }
+
+
+    def __init__(self, goban, socket):
+        self.goban = goban
+        self.goban_lock = threading.Lock()
+        self.socket = GTPSocket(socket)
+        self.send_lock = threading.Lock()
+
+        GTPSocket.known_cmds.extend(dispatcher.keys())
+
+
+    def run(self):
+        pass
diff --git a/pygo.py b/pygo.py
index ace1ec5..bf64307 100755
--- a/pygo.py
+++ b/pygo.py
@@ -7,6 +7,7 @@ import sys
 sys.path.append('lib/')
 
 import ConfigParser
+
 import goban
 import pygogui