from common import stanza_session from common import xmpp import pygtk pygtk.require('2.0') import gtk from gtk import gdk import cairo # implements games_ns = 'http://jabber.org/protocol/games' class InvalidMove(Exception): pass class TicTacToeSession(stanza_session.StanzaSession): # initiate a session def begin(self, rows = 3, cols = 3, role_s = 'x'): self.rows = rows self.cols = cols self.role_s = role_s self.strike = 3 if self.role_s == 'x': self.role_o = 'o' else: self.role_o = 'x' self.send_invitation() self.next_move_id = 1 self.received = self.wait_for_invite_response def send_invitation(self): msg = xmpp.Message() invite = msg.NT.invite invite.setNamespace(games_ns) game = invite.NT.game game.setAttr('var', games_ns + '/tictactoe') x = xmpp.DataForm(typ='submit') game.addChild(node=x) self.send(msg) def read_invitation(self, msg): invite = msg.getTag('invite', namespace=games_ns) game = invite.getTag('game') x = game.getTag('x', namespace='jabber:x:data') form = xmpp.DataForm(node=x) if form.getField('role'): self.role_o = form.getField('role').getValues()[0] else: self.role_o = 'x' if form.getField('rows'): self.rows = int(form.getField('rows').getValues()[0]) else: self.rows = 3 if form.getField('cols'): self.cols = int(form.getField('cols').getValues()[0]) else: self.cols = 3 # number in a row needed to win if form.getField('strike'): self.strike = int(form.getField('strike').getValues()[0]) else: self.strike = 3 # received an invitation def invited(self, msg): self.read_invitation(msg) # XXX prompt user # "accept, reject, ignore" # the number of the move about to be made self.next_move_id = 1 # display the board self.board = TicTacToeBoard(self, self.rows, self.cols) # accept the invitation, join the game response = xmpp.Message() join = response.NT.join join.setNamespace(games_ns) self.send(response) if self.role_o == 'x': self.role_s = 'o' self.their_turn() else: self.role_s = 'x' self.role_o = 'o' self.our_turn() # just sent an invitation, expecting a reply def wait_for_invite_response(self, msg): if msg.getTag('join', namespace=games_ns): self.board = TicTacToeBoard(self, self.rows, self.cols) if self.role_s == 'x': self.our_turn() else: self.their_turn() elif msg.getTag('decline', namespace=games_ns): # XXX notify the user # XXX end session pass # silently ignores any received messages def ignore(self, msg): pass def game_over(self, msg): invite = msg.getTag('invite', namespace=games_ns) # ignore messages unless they're renewing the game if invite and invite.getAttr('type') == 'renew': self.invited(msg) def wait_for_move(self, msg): turn = msg.getTag('turn', namespace=games_ns) move = turn.getTag('move', namespace='http://jabber.org/protocol/games/tictactoe') row = int(move.getAttr('row')) col = int(move.getAttr('col')) id = int(move.getAttr('id')) if id != self.next_move_id: print 'unexpected move id, lost a move somewhere?' return try: self.board.mark(row, col, self.role_o) except InvalidMove, e: # received an invalid move, end the game. # XXX notify the user self.terminate('cheating') return # check win conditions if self.board.check_for_strike(self.role_o, row, col, self.strike): self.lost() elif self.board.full(): self.drawn() else: self.next_move_id += 1 self.our_turn() def is_my_turn(self): # XXX not great semantics return self.received == self.ignore def our_turn(self): # ignore messages until we've made our move self.received = self.ignore self.board.set_title('your turn') def their_turn(self): self.received = self.wait_for_move self.board.set_title('their turn') # called when the board receives input def move(self, row, col): try: self.board.mark(row, col, self.role_s) except InvalidMove, e: print 'you made an invalid move' return self.send_move(row, col) # check win conditions if self.board.check_for_strike(self.role_s, row, col, self.strike): self.won() elif self.board.full(): self.drawn() else: self.next_move_id += 1 self.their_turn() # sends a move message def send_move(self, row, column): msg = xmpp.Message() msg.setType('chat') turn = msg.NT.turn turn.setNamespace(games_ns) move = turn.NT.move move.setNamespace(games_ns+'/tictactoe') move.setAttr('row', str(row)) move.setAttr('col', str(column)) move.setAttr('id', str(self.next_move_id)) self.send(msg) # sends a termination message and ends the game def terminate(self, reason): msg = xmpp.Message() terminate = msg.NT.terminate terminate.setNamespace(games_ns) terminate.setAttr('reason', reason) self.send(msg) self.received = self.game_over def won(self): self.terminate('won') self.board.won() def lost(self): self.terminate('lost') self.board.lost() def drawn(self): self.terminate('draw') self.board.drawn() class TicTacToeBoard: def __init__(self, session, rows, cols): self.session = session self.state = 'None' self.rows = rows self.cols = cols self.board = [ [None] * self.cols for r in xrange(self.rows) ] self.setup_window() def check_for_strike(self, p, r, c, strike): # number in a row: up and down, left and right tallyI = 0 tally_ = 0 # number in a row: diagonal # (imagine L or F as two sides of a right triangle: L\ or F/) tallyL = 0 tallyF = 0 # convert real columns to internal columns r -= 1 c -= 1 for d in xrange(-strike, strike): # vertical check try: tallyI = tallyI + 1 if self.board[r+d][c] == p else 0 except IndexError: pass # horizontal check try: tally_ = tally_ + 1 if self.board[r][c+d] == p else 0 except IndexError: pass # diagonal checks try: tallyL = tallyL + 1 if self.board[r+d][c+d] == p else 0 except IndexError: pass try: tallyF = tallyF + 1 if self.board[r+d][c-d] == p else 0 except IndexError: pass if any([t == strike for t in (tallyL, tallyF, tallyI, tally_)]): return True return False # is the board full? def full(self): for r in xrange(self.rows): for c in xrange(self.cols): if self.board[r][c] == None: return False return True def setup_window(self): self.win = gtk.Window() self.title_prefix = 'tic-tac-toe with %s' % self.session.jid self.set_title() self.win.set_app_paintable(True) self.win.add_events(gdk.BUTTON_PRESS_MASK) self.win.connect('button-press-event', self.clicked) self.win.connect('expose-event', self.expose) self.win.show_all() def clicked(self, widget, event): if not self.session.is_my_turn(): return (height, width) = widget.get_size() # convert click co-ordinates to row and column row_height = height // self.rows col_width = width // self.cols row = int(event.y // row_height) + 1 column = int(event.x // col_width) + 1 self.session.move(row, column) # this actually draws the board def expose(self, widget, event): win = widget.window cr = win.cairo_create() cr.set_source_rgb(1.0, 1.0, 1.0) cr.set_operator(cairo.OPERATOR_SOURCE) cr.paint() (width, height) = widget.get_size() row_height = height // self.rows col_width = width // self.cols for i in xrange(self.rows): for j in xrange(self.cols): if self.board[i][j] == 'x': self.draw_x(cr, i, j, row_height, col_width) elif self.board[i][j] == 'o': self.draw_o(cr, i, j, row_height, col_width) # XXX draw 'won', 'lost', 'draw' def draw_x(self, cr, row, col, row_height, col_width): cr.set_source_rgb(0, 0, 0) top = row_height * (row + 0.2) bottom = row_height * (row + 0.8) left = col_width * (col + 0.2) right = col_width * (col + 0.8) cr.set_line_width(row_height / 5) cr.move_to(left, top) cr.line_to(right, bottom) cr.move_to(right, top) cr.line_to(left, bottom) cr.stroke() def draw_o(self, cr, row, col, row_height, col_width): cr.set_source_rgb(0, 0, 0) x = col_width * (col + 0.5) y = row_height * (row + 0.5) cr.arc(x, y, row_height/4, 0, 2.0*3.2) # slightly further than 2*pi cr.set_line_width(row_height / 5) cr.stroke() # mark a move on the board def mark(self, row, column, player): if self.board[row-1][column-1]: raise InvalidMove else: self.board[row-1][column-1] = player self.win.queue_draw() def set_title(self, suffix = None): str = self.title_prefix if suffix: str += ': ' + suffix self.win.set_title(str) def won(self): self.state = 'won' self.set_title('you won!') self.win.queue_draw() def lost(self): self.state = 'lost' self.set_title('you lost.') self.win.queue_draw() def drawn(self): self.state = 'drawn' self.win.set_title(self.title_prefix + ': a draw.') self.win.queue_draw()