home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / glchess / ggz / client.py < prev    next >
Encoding:
Python Source  |  2009-04-14  |  21.9 KB  |  677 lines

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import protocol
  4. import xml.sax.saxutils
  5. import gettext
  6.  
  7. _ = gettext.gettext
  8.  
  9. class Channel:
  10.     """
  11.     """    
  12.         
  13.     def logXML(self, text):
  14.         pass
  15.         
  16.     def logBinary(self, data):
  17.         pass
  18.     
  19.     def connect(self, host, port):
  20.         pass
  21.     
  22.     def send(self, data, isBinary = False):
  23.         pass
  24.     
  25.     def close(self):
  26.         pass
  27.     
  28. class ChannelFeedback:
  29.     """
  30.     """
  31.     
  32.     def registerIncomingData(self, data):
  33.         pass
  34.  
  35.     def onSessionEnded(self):
  36.         pass
  37.     
  38.     def closed(self, errno = 0):
  39.         pass
  40.     
  41. class ClientFeedback:
  42.     
  43.     def setBusy(self, isBusy):
  44.         """Called when the client is busy (unable to take requests)"""
  45.         pass
  46.     
  47.     def onConnected(self):
  48.         pass
  49.  
  50.     def onDisconnected(self, reason):
  51.         pass
  52.     
  53.     def openChannel(self, feedback):
  54.         """Open a channel to the GGZ server.
  55.         
  56.         'feedback' is the object to notify data received on this channel.
  57.         """
  58.         pass
  59.     
  60.     def getLogin(self):
  61.         """Called when the login credentials are required.
  62.         
  63.         Returns the username and password to log in with.
  64.         If the password is None then log in as guest.
  65.         """
  66.         return ('test', None)
  67.     
  68.     def getPassword(self, username):
  69.         """Called when a password is required.
  70.         
  71.         'username' is the username the password is required for.
  72.         """
  73.         return None
  74.     
  75.     def onMOTD(self, motd):
  76.         """Called when the message of the day is received.
  77.         
  78.         'motd' is the message received.
  79.         """
  80.         pass
  81.     
  82.     def roomAdded(self, room):
  83.         """Called when a room is added.
  84.         
  85.         'room' the new room.
  86.         """
  87.         pass
  88.     
  89.     def roomUpdated(self, room):
  90.         pass
  91.  
  92.     def roomJoined(self, room):
  93.         pass
  94.     
  95.  
  96.     def tableAdded(self, table):
  97.         pass
  98.  
  99.     def tableUpdated(self, table):
  100.         pass
  101.  
  102.     def tableRemoved(self, table):
  103.         pass
  104.     
  105.  
  106.     def playerAdded(self, player):
  107.         pass
  108.  
  109.     def playerRemoved(self, player):
  110.         pass
  111.     
  112.     
  113.     def onChat(self, chatType, sender, text):
  114.         pass
  115.  
  116. class Game:
  117.     id = ''
  118.     
  119.     name = ''
  120.     version = ''
  121.     
  122.     protocol_engine = ''
  123.     protocol_version = ''
  124.     
  125.     nPlayers = 0
  126.     
  127.     author = ''
  128.     url = ''
  129.  
  130. class Room:
  131.     id = ''
  132.     
  133.     game = None
  134.     
  135.     nPlayers = 0
  136.  
  137. class Table:
  138.     id = ''
  139.     
  140.     room = ''
  141.     
  142.     game = None
  143.     
  144.     status = ''
  145.     
  146.     description = ''
  147.     
  148.     def __init__(self, nSeats):
  149.         self.seats = []
  150.         for i in xrange(nSeats):
  151.             seat = Seat()
  152.             self.seats.append(seat)
  153.  
  154. class Seat:
  155.     type = ''
  156.     
  157.     user = ''
  158.  
  159. class Player:
  160.     name = ''
  161.     
  162.     type = ''
  163.     
  164.     table = None
  165.     
  166.     perms = 0
  167.     
  168.     lag = 0
  169.     
  170.     room = None
  171.     lastRoom = None
  172.     
  173. class MainChannel(ChannelFeedback, protocol.ParserFeedback):
  174.     def __init__(self, client):
  175.         self.client = client
  176.         self.decoder = protocol.Decoder(self)
  177.  
  178.     def registerIncomingData(self, data):
  179.         self.controller.logXML(data)
  180.         while len(data) > 0:
  181.             data = self.decoder.feed(data)
  182.  
  183.     def send(self, data, isBinary = False):
  184.         self.controller.send(data, isBinary)
  185.  
  186.     def onConnected(self):
  187.         assert(self.client.state is self.client.STATE_DISCONNECTED)
  188.         self.client.feedback.onConnected()
  189.         self.client.setState(self.client.STATE_START_SESSION)
  190.         
  191.         try:
  192.             language = os.environ['LANG']
  193.         except KeyError:
  194.             language = 'C'
  195.  
  196.         self.send("<?xml version='1.0' encoding='UTF-8'?>\n")
  197.         self.send("<SESSION>\n")
  198.         self.send("<LANGUAGE>%s</LANGUAGE>\n" % xml.sax.saxutils.escape(language))
  199.         (self.client.username, self.client.password) = self.client.feedback.getLogin()
  200.         if self.client.password is None:
  201.             self.client._loginGuest(self.client.username)
  202.         else:
  203.             self.client._login(self.client.username, self.client.password)
  204.         
  205.     def onResult(self, action, code):
  206.         if action == 'login':
  207.             if code == 'ok':
  208.                 self.client.setState(self.client.STATE_LIST_GAMES)
  209.             else:
  210.                 if code == 'usr lookup':
  211.                     # Translators: GGZ disconnection error when the supplied password is incorrect
  212.                     self.client.close(_('Incorrect password'))
  213.                     #FIXME: Prompt for a password
  214.                     #self.client.setState(self.client.STATE_GET_PASSWORD)
  215.  
  216.                 elif code == 'already logged in':
  217.                     # Translators: GGZ disconnection error when the selected account is already in use
  218.                     self.client.close(_('Account in use'))
  219.                     #FIXME: If guest then prompt for a new user or mangle username until one can be found
  220.                     
  221.                 elif code == 'wrong login type':
  222.                     self.client.setState(self.client.STATE_GET_PASSWORD)
  223.                 
  224.                 else:
  225.                     self.client.close(code)
  226.  
  227.         elif action == 'enter':
  228.             if code != 'ok':
  229.                 print 'Failed to enter room'
  230.                 self.client.setState(self.client.STATE_READY)
  231.                 return
  232.             
  233.             if self.client.room is not None:
  234.                 self.client.room.nPlayers -= 1
  235.                 self.client.feedback.roomUpdated(self.client.room)
  236.                 
  237.             room = self.client.enteringRoom
  238.  
  239.             self.client.room = room
  240.             self.client.players = {}
  241.             self.client.tables = {}
  242.             room.nPlayers = 0
  243.             self.client.setState(self.client.STATE_LIST_TABLES)
  244.  
  245.         elif action == 'list':
  246.             if self.client.state is self.client.STATE_LIST_GAMES:
  247.                 self.client.setState(self.client.STATE_LIST_ROOMS)
  248.                 
  249.             elif self.client.state is self.client.STATE_LIST_ROOMS:
  250.                 for room in self.client.rooms.itervalues():
  251.                     if room.game is None:
  252.                         break
  253.                 # FIXME: Check have valid room, otherwise go to room 0
  254.                 
  255.                 self.client.setState(self.client.STATE_ENTERING_ROOM)
  256.                 self.client.enteringRoom = room
  257.                 self.send("<ENTER ROOM='%s'/>\n" % room.id)
  258.                 
  259.             elif self.client.state is self.client.STATE_LIST_TABLES:
  260.                 self.client.setState(self.client.STATE_LIST_PLAYERS)
  261.                 
  262.             elif self.client.state is self.client.STATE_LIST_PLAYERS:
  263.                 self.client.setState(self.client.STATE_READY)
  264.                 self.client.feedback.roomEntered(self.client.enteringRoom)
  265.  
  266.         elif action == 'chat':
  267.             pass# FIXME: could be AT_TABLE self.client.setState(self.client.STATE_READY)
  268.  
  269.         elif action == 'launch':
  270.             self.client.setState(self.client.STATE_READY)
  271.             
  272.         elif action == 'join':
  273.             if code == 'ok':
  274.                 self.client.setState(self.client.STATE_AT_TABLE)
  275.             elif code == 'table full':
  276.                 print 'Failed to join table: table full'
  277.                 self.client.setState(self.client.STATE_READY)
  278.             else:
  279.                 print 'Unknown join result: %s' % action
  280.                 
  281.         elif action == 'leave':
  282.             if code == 'ok':
  283.                 self.client.setState(self.client.STATE_READY)
  284.                 pass # TODO
  285.             else:
  286.                 print 'Unknown leave result: %s' % action
  287.  
  288.         else:
  289.             print 'Unknown result: %s %s' % (action, code)
  290.  
  291.     def onMOTD(self, motd):
  292.         self.client.feedback.onMOTD(motd)
  293.  
  294.     def onChat(self, chatType, sender, text):
  295.         self.client.feedback.onChat(chatType, sender, text)
  296.         
  297.     def onJoin(self, tableId, isSpectator):
  298.         try:
  299.             table = self._getTable(tableId)
  300.         except KeyError:
  301.             print "Unknown JOIN with TABLE='%s'" % tableId
  302.             return
  303.         self.client.setState(self.client.STATE_AT_TABLE)
  304.         g = self.client.feedback.onJoin(table, isSpectator, self.client.channel)
  305.         self.client.channel.setGame(g)
  306.         
  307.     def onLeave(self, reason):
  308.         self.client.feedback.onLeave(reason)
  309.         
  310.     def gameAdded(self, gameId, name, version, author, url, numPlayers, protocol_engine, protocol_version):
  311.         game = Game()
  312.         game.id = gameId
  313.         game.name = name
  314.         game.version = version
  315.         game.author = author
  316.         game.url = url
  317.         game.numPlayers = numPlayers # FIXME: Make min/max (e.g. can be '1..3')
  318.         game.protocol_engine = protocol_engine
  319.         game.protocol_version = protocol_version
  320.         self.client.games[gameId] = game
  321.  
  322.     def roomAdded(self, roomId, gameId, name, description, nPlayers):
  323.         try:
  324.             game = self._getGame(gameId, optional = True)
  325.         except KeyError:
  326.             print "Unknown ROOM ADD with GAME='%s'" % gameId
  327.             return
  328.         room = Room()
  329.         room.id = roomId
  330.         room.game = game
  331.         room.name = name
  332.         room.description = description
  333.         room.nPlayers = int(nPlayers)
  334.         self.client.rooms[roomId] = room
  335.         self.client.feedback.roomAdded(room)
  336.  
  337.     def roomPlayersUpdate(self, roomId, nPlayers):
  338.         try:
  339.             room = self._getRoom(roomId)
  340.         except KeyError:
  341.             print "Unknown ROOM PLAYER UPDATE with ROOM='%s'" % roomId
  342.             return
  343.         room.nPlayers = int(nPlayers)
  344.         self.client.feedback.roomUpdated(room)
  345.  
  346.     def tableAdded(self, roomId, tableId, gameId, status, nSeats, description): 
  347.         try:
  348.             room = self._getRoom(roomId)
  349.             game = self._getGame(gameId)
  350.         except KeyError:
  351.             print "Unknown TABLE ADD with ROOM='%s' GAME='%s'" % (roomId, gameId)
  352.             return
  353.         table = Table(int(nSeats))
  354.         table.id = tableId
  355.         table.room = room
  356.         table.game = game
  357.         table.status = status
  358.         table.description = description
  359.         self.client.tables[tableId] = table
  360.         self.client.feedback.tableAdded(table)
  361.  
  362.     def tableStatusChanged(self, tableId, status):
  363.         try:
  364.             table = self._getTable(tableId)
  365.         except KeyError:
  366.             print "Unknown TABLE STATUS with TABLE='%s'" % tableId
  367.             return
  368.         table.status = status
  369.         self.client.feedback.tableUpdated(table)
  370.  
  371.     def seatChanged(self, roomId, tableId, seatId, seatType, user):
  372.         try:
  373.             table = self._getTable(tableId)
  374.         except KeyError:
  375.             print "Unknown SEAT CHANGE with TABLE='%s'" % tableId
  376.             return
  377.         if table.room.id != roomId:
  378.             return
  379.         seat = table.seats[int(seatId)]
  380.         seat.type = seatType
  381.         seat.user = user
  382.         self.client.feedback.tableUpdated(table)
  383.  
  384.     def tableRemoved(self, tableId):
  385.         try:
  386.             table = self.client.tables.pop(tableId)
  387.         except KeyError: 
  388.             print "Unknown TABLE REMOVE with TABLE='%s'" % tableId           
  389.             # We do not know of this table - this could occur if we receive a
  390.             # table remove event before we get the table list.
  391.             return
  392.         self.client.feedback.tableRemoved(table)
  393.         
  394.     def onPlayerList(self, roomId, players):
  395.         try:
  396.             room = self._getRoom(roomId)
  397.             for p in players:
  398.                 _ = self._getTable(p.tableId, optional = True)
  399.         except KeyError: 
  400.             print "Unknown PLAYER LIST with ROOM='%s'" % roomId
  401.             return
  402.         self.client.players = {}
  403.         for p in players:
  404.             player = Player()
  405.             player.name = p.name
  406.             player.type = p.type
  407.             player.table = self._getTable(p.tableId, optional = True)
  408.             player.perms = p.perms
  409.             player.lag = p.lag
  410.             player.room = room
  411.             self.client.players[player.name] = player
  412.  
  413.         room.nPlayers = len(players)
  414.         self.client.feedback.roomUpdated(room)
  415.  
  416.     def playerAdded(self, name, playerType, tableId, perms, lag, roomId, fromRoomId):
  417.         try:
  418.             room = self._getRoom(roomId, optional = True)
  419.             lastRoom = self.client.rooms[fromRoomId]
  420.             table = self._getTable(tableId, optional = True)
  421.         except KeyError:
  422.             print "Unknown PLAYER ADD with ROOM='%s' LASTROOM='%s' TABLE='%s'" % (roomId, fromRoomId, tableId)
  423.             return
  424.         player = Player()
  425.         player.name = name
  426.         player.type = playerType
  427.         player.table = table
  428.         player.perms = perms
  429.         player.lag = lag
  430.         player.room = room
  431.         player.lastRoom = lastRoom
  432.         self.client.players[player.name] = player
  433.  
  434.         if player.lastRoom is not None:
  435.             player.lastRoom.nPlayers -= 1
  436.             self.client.feedback.roomUpdated(player.lastRoom)
  437.         if player.room is not None:
  438.             player.room.nPlayers += 1
  439.             self.client.feedback.roomUpdated(player.room)
  440.         self.client.feedback.playerAdded(player)
  441.  
  442.     def playerRemoved(self, name, roomId, toRoomId):
  443.         try:
  444.             player = self.client.players.pop(name)
  445.             room = self._getRoom(toRoomId, optional = True)
  446.         except KeyError:
  447.             print "Unknown PLAYER REMOVE with NAME='%s' ROOM='%s' TOROOM='%s'" % (name, roomId, toRoomId)
  448.             # We do not know of this player - this could occur if we receive a
  449.             # player remove event before we get the player list.
  450.             return
  451.             
  452.         player.lastRoom = player.room
  453.         player.room = room
  454.         if player.room is not None:
  455.             player.room.nPlayers += 1
  456.             self.client.feedback.roomUpdated(player.room)
  457.         if player.lastRoom is not None:
  458.             player.lastRoom.nPlayers -= 1
  459.             self.client.feedback.roomUpdated(player.lastRoom)
  460.         self.client.feedback.playerRemoved(player)
  461.  
  462.     def closed(self, errno = 0):
  463.         # Translators: GGZ disconnection error when the network link has broken. %s is the system provided error
  464.         self.client.close(_('Connection closed: %s') % os.strerror(errno))
  465.        
  466.     def _getGame(self, gameId, optional = False):
  467.         if optional and gameId == '-1':
  468.             return None
  469.         return self.client.games[gameId]
  470.  
  471.     def _getRoom(self, roomId, optional = False):
  472.         if optional and roomId == '-1':
  473.             return None
  474.         return self.client.rooms[roomId]    
  475.         
  476.     def _getTable(self, tableId, optional = False):
  477.         if optional and tableId == '-1':
  478.             return None
  479.         return self.client.tables[tableId]
  480.     
  481. class GameChannel(ChannelFeedback, protocol.ParserFeedback):
  482.     
  483.     def __init__(self, client, command):
  484.         self.client = client
  485.         self.command = command
  486.         self.inSession = True
  487.         self.decoder = protocol.Decoder(self)
  488.         self.buffer = ''
  489.         self.game = None
  490.         
  491.     def setGame(self, game):
  492.         self.game = game
  493.         if len(self.buffer) > 0:
  494.             self.game.registerIncomingData(self.buffer)
  495.         self.buffer = ''
  496.  
  497.     def registerIncomingData(self, data):
  498.         while self.inSession and len(data) > 0:
  499.             remainder = self.decoder.feed(data)
  500.             self.controller.logXML(data[:len(data) - len(remainder)])
  501.             data = remainder
  502.  
  503.         if len(data) == 0:
  504.             return
  505.         
  506.         self.controller.logBinary(data)
  507.  
  508.         if self.game is None:
  509.             self.buffer += data
  510.         else:
  511.             self.game.registerIncomingData(data)
  512.  
  513.     def onDisconnected(self, reason):
  514.         print 'Disconnected: %s' % reason
  515.  
  516.     def send(self, data, isBinary = False):
  517.         self.controller.send(data, isBinary)
  518.  
  519.     def onConnected(self):
  520.         self.send("<?xml version='1.0' encoding='UTF-8'?>\n")
  521.         self.send("<SESSION>\n")
  522.         self.send("<LANGUAGE>en_NZ.UTF-8</LANGUAGE>\n")
  523.         self.send("<CHANNEL ID='%s' /></SESSION>\n" % self.client.username)
  524.         
  525.     def onSessionEnded(self):
  526.         self.inSession = False
  527.         self.client.mainChannel.send(self.command)
  528.  
  529.     def closed(self, errno = 0):
  530.         print 'SEVERE: GGZ channel closed'
  531.  
  532. class Client:
  533.     
  534.     STATE_DISCONNECTED        = 'DISCONNECTED'
  535.     STATE_START_SESSION       = 'START_SESSION'
  536.     STATE_LOGIN               = 'LOGIN'
  537.     STATE_GET_PASSWORD        = 'GET_PASSWORD'    
  538.     STATE_LIST_GAMES          = 'LIST_GAMES'
  539.     STATE_LIST_ROOMS          = 'LIST_ROOMS'
  540.     STATE_READY               = 'READY'
  541.     STATE_JOIN_TABLE_CHANNEL  = 'JOIN_TABLE_CHANNEL'
  542.     STATE_JOIN_TABLE          = 'JOIN_TABLE'
  543.     STATE_START_TABLE_CHANNEL = 'START_TABLE_CHANNEL'
  544.     STATE_START_TABLE         = 'START_TABLE'
  545.     STATE_AT_TABLE            = 'AT_TABLE'
  546.     STATE_LEAVE_TABLE         = 'LEAVE_TABLE'
  547.     STATE_ENTERING_ROOM       = 'ENTERING_ROOM'
  548.     STATE_LIST_TABLES         = 'LIST_TABLES'
  549.     STATE_LIST_PLAYERS        = 'LIST_PLAYERS'
  550.  
  551.     def __init__(self, feedback):
  552.         self.feedback = feedback
  553.         self.commands = []
  554.         self.sending = False
  555.         self.games = {}
  556.         self.rooms = {}
  557.         self.tables = {}
  558.         self.players = {}
  559.         self.room = None
  560.         self.state = self.STATE_DISCONNECTED
  561.         
  562.     def isReady(self):
  563.         """Check if ready for new requests.
  564.         
  565.         Returns True if can make a new request.
  566.         """
  567.         return self.state is self.STATE_READY
  568.  
  569.     def close(self, error):
  570.         self.disconnectionError = error
  571.         self.setState(self.STATE_DISCONNECTED)
  572.         
  573.     def isBusy(self):
  574.         return not (self.state is self.STATE_READY or self.state is self.STATE_AT_TABLE or self.state is self.STATE_DISCONNECTED)
  575.  
  576.     def setState(self, state):
  577.         print 'Changing state from %s to %s' % (self.state, state)
  578.         self.state = state
  579.  
  580.         self.feedback.setBusy(self.isBusy())
  581.         
  582.         if state is self.STATE_LIST_GAMES:
  583.             self.mainChannel.send("<LIST TYPE='game' FULL='true'/>\n")
  584.         elif state is self.STATE_LIST_ROOMS:
  585.             self.mainChannel.send("<LIST TYPE='room' FULL='true'/>\n")
  586.         elif state is self.STATE_LIST_TABLES:
  587.             self.mainChannel.send("<LIST TYPE='table'/>\n")
  588.         elif state is self.STATE_LIST_PLAYERS:
  589.             self.mainChannel.send("<LIST TYPE='player'/>\n")
  590.         elif state is self.STATE_GET_PASSWORD:
  591.             self.feedback.getPassword(self.username)
  592.         elif state is self.STATE_DISCONNECTED:
  593.             self.feedback.onDisconnected(self.disconnectionError)
  594.             self.mainChannel.controller.close()
  595.  
  596.     def start(self):
  597.         assert(self.state is self.STATE_DISCONNECTED)
  598.         self.mainChannel = MainChannel(self)
  599.         self.mainChannel.controller = self.feedback.openChannel(self.mainChannel)
  600.         
  601.     def setPassword(self, password):
  602.         assert(self.state is self.STATE_GET_PASSWORD)
  603.         if password is None:
  604.             # Translators: GGZ disconnection error when a password was required for the selected account
  605.             self.close(_('A password is required'))
  606.         else:
  607.             self._login(self.username, password)
  608.         
  609.     def _login(self, username, password):
  610.         assert(self.state is self.STATE_START_SESSION or self.state is self.STATE_GET_PASSWORD)
  611.         self.setState(self.STATE_LOGIN)
  612.         username = xml.sax.saxutils.escape(username)
  613.         password = xml.sax.saxutils.escape(password)
  614.         self.mainChannel.send("<LOGIN TYPE='normal'><NAME>%s</NAME><PASSWORD>%s</PASSWORD></LOGIN>\n" % (username, password));
  615.         
  616.     def _loginGuest(self, username):
  617.         assert(self.state is self.STATE_START_SESSION)
  618.         self.setState(self.STATE_LOGIN)
  619.         username = xml.sax.saxutils.escape(username)
  620.         self.mainChannel.send("<LOGIN TYPE='guest'><NAME>%s</NAME></LOGIN>\n" % username);
  621.         
  622.     def _loginNew(self, username, password, email):
  623.         assert(self.state is self.STATE_START_SESSION)
  624.         self.setState(self.STATE_LOGIN)
  625.         username = xml.sax.saxutils.escape(username)
  626.         password = xml.sax.saxutils.escape(password)
  627.         email = xml.sax.saxutils.escape(email)
  628.         self.mainChannel.send("<LOGIN TYPE='first'><NAME>%s</NAME><PASSWORD>%s</PASSWORD><EMAIL>%s</EMAIL></LOGIN>\n" % (username, password, email));
  629.         
  630.     def enterRoom(self, room):
  631.         if self.state is self.STATE_AT_TABLE:
  632.             print 'At table'
  633.             return
  634.         else:
  635.             assert(self.state is self.STATE_READY)
  636.         self.setState(self.STATE_ENTERING_ROOM)
  637.         self.enteringRoom = room
  638.         self.mainChannel.send("<ENTER ROOM='%s'/>\n" % room.id)
  639.         
  640.     def startTable(self, gameId, description, player):
  641.         if self.state is not self.STATE_READY:
  642.             print 'Unable to start table'
  643.             return
  644.         self.setState(self.STATE_START_TABLE)
  645.         
  646.         # Seat types are 'open', 'bot' or 'reserved' or 'player' (latter two have player name in them)
  647.         command = "<LAUNCH>\n"
  648.         command += "<TABLE GAME='%s' SEATS='2'>\n" % gameId
  649.         command += "<DESC>%s</DESC>\n" % xml.sax.saxutils.escape(description)
  650.         command += "<SEAT NUM='0' TYPE='reserved'>%s</SEAT>\n" % player
  651.         command += "<SEAT NUM='1' TYPE='open'/>\n"
  652.         command += "</TABLE>\n"
  653.         command += "</LAUNCH>\n"
  654.         
  655.         self.channel = GameChannel(self, command)
  656.         self.channel.controller = self.feedback.openChannel(self.channel)
  657.         
  658.     def joinTable(self, table):
  659.         if self.state is self.STATE_AT_TABLE:
  660.             print 'Already at table'
  661.             return
  662.         
  663.         assert(self.state is self.STATE_READY)
  664.         self.setState(self.STATE_JOIN_TABLE)
  665.         
  666.         command = "<JOIN TABLE='%s' SPECTATOR='false'/>\n" % table.id
  667.         self.channel = GameChannel(self, command)
  668.         self.channel.controller = self.feedback.openChannel(self.channel)
  669.  
  670.     def leaveTable(self):
  671.         assert(self.state is self.STATE_AT_TABLE)
  672.         self.setState(self.STATE_LEAVE_TABLE)
  673.         self.mainChannel.send("<LEAVE FORCE='true'/>\n")
  674.         
  675.     def sendChat(self, text):
  676.         self.mainChannel.send("<CHAT TYPE='normal'>%s</CHAT>\n" % xml.sax.saxutils.escape(text))
  677.