plugin.py 23.6 KB
Newer Older
Yann Leboulanger's avatar
Yann Leboulanger committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
## plugins/tictactoe/plugin.py
##
## Copyright (C) 2011 Yann Leboulanger <asterix AT lagaule.org>
##
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##

'''
Tictactoe plugin.

:author: Yann Leboulanger <asterix@lagaule.org>
:since: 21 November 2011
:copyright: Copyright (2011) Yann Leboulanger <asterix@lagaule.org>
:license: GPL
'''

29 30 31
import string
import itertools
import random
Yann Leboulanger's avatar
Yann Leboulanger committed
32

33 34 35
import nbxmpp
from gi.repository import Gtk
from gi.repository import Gdk
36 37
from gi.repository import Gio
from gi.repository import GLib
38 39 40
import gi
gi.require_version('PangoCairo', '1.0')
from gi.repository import PangoCairo
41 42 43 44 45 46

from gajim.common import helpers
from gajim.common import app
from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import log_calls, log
from gajim.plugins.gui import GajimPluginConfigDialog
47 48 49 50
from gajim import chat_control
from gajim.common import ged
from gajim import dialogs
from gajim.common.connection_handlers_events import InformationEvent
Yann Leboulanger's avatar
Yann Leboulanger committed
51

52 53 54 55 56 57
# Since Gajim 1.1.0 _() has to be imported
try:
    from gajim.common.i18n import _
except ImportError:
    pass

Yann Leboulanger's avatar
Yann Leboulanger committed
58 59 60 61 62 63 64
NS_GAMES = 'http://jabber.org/protocol/games'
NS_GAMES_TICTACTOE = NS_GAMES + '/tictactoe'

class TictactoePlugin(GajimPlugin):
    @log_calls('TictactoePlugin')
    def init(self):
        self.description = _('Play Tictactoe.')
65
        self.config_dialog = TictactoePluginConfigDialog(self)
Yann Leboulanger's avatar
Yann Leboulanger committed
66
        self.events_handlers = {
67 68
            'decrypted-message-received': (
                ged.PREGUI, self._nec_decrypted_message_received),
Yann Leboulanger's avatar
Yann Leboulanger committed
69
        }
70

Yann Leboulanger's avatar
Yann Leboulanger committed
71
        self.gui_extension_points = {
72 73 74 75
            'chat_control': (self.connect_with_chat_control,
                             self.disconnect_from_chat_control),
            'chat_control_base_update_toolbar': (
                self.update_button_state, None),
76
            'update_caps': (self._update_caps, None),
Yann Leboulanger's avatar
Yann Leboulanger committed
77
        }
78

79 80 81
        self.config_default_values = {
            'board_size': (5, ''),
        }
82

Yann Leboulanger's avatar
Yann Leboulanger committed
83
        self.controls = []
84
        self.announce_caps = True
Yann Leboulanger's avatar
Yann Leboulanger committed
85 86

    @log_calls('TictactoePlugin')
87 88 89 90 91 92 93
    def _update_caps(self, account):
        if not self.announce_caps:
            return
        if NS_GAMES not in app.gajim_optional_features[account]:
            app.gajim_optional_features[account].append(NS_GAMES)
        if NS_GAMES_TICTACTOE not in app.gajim_optional_features[account]:
            app.gajim_optional_features[account].append(NS_GAMES_TICTACTOE)
Yann Leboulanger's avatar
Yann Leboulanger committed
94 95 96

    @log_calls('TictactoePlugin')
    def activate(self):
97 98 99 100
        for account in app.caps_hash:
            if app.caps_hash[account] != '':
                self.announce_caps = True
                helpers.update_optional_features(account)
Yann Leboulanger's avatar
Yann Leboulanger committed
101 102 103

    @log_calls('TictactoePlugin')
    def deactivate(self):
104 105
        self.announce_caps = False
        helpers.update_optional_features()
Yann Leboulanger's avatar
Yann Leboulanger committed
106 107 108 109 110 111 112

    @log_calls('TictactoePlugin')
    def connect_with_chat_control(self, control):
        if isinstance(control, chat_control.ChatControl):
            base = Base(self, control)
            self.controls.append(base)
            # Already existing session?
113
            conn = app.connections[control.account]
Yann Leboulanger's avatar
Yann Leboulanger committed
114 115 116 117 118
            sessions = conn.get_sessions(control.contact.jid)
            tictactoes = [s for s in sessions if isinstance(s,
                TicTacToeSession)]
            if tictactoes:
                base.tictactoe = tictactoes[0]
119
                base.enable_action(True)
Yann Leboulanger's avatar
Yann Leboulanger committed
120 121 122 123 124 125 126 127 128 129 130 131 132

    @log_calls('TictactoePlugin')
    def disconnect_from_chat_control(self, chat_control):
        for base in self.controls:
            base.disconnect_from_chat_control()
        self.controls = []

    @log_calls('TictactoePlugin')
    def update_button_state(self, control):
        for base in self.controls:
            if base.chat_control == control:
                if control.contact.supports(NS_GAMES) and \
                control.contact.supports(NS_GAMES_TICTACTOE):
133
                    base.enable_action(True)
Yann Leboulanger's avatar
Yann Leboulanger committed
134
                else:
135
                    base.enable_action(False)
Yann Leboulanger's avatar
Yann Leboulanger committed
136 137 138 139 140 141 142 143 144 145

    @log_calls('TictactoePlugin')
    def show_request_dialog(self, obj, session):
        def on_ok():
            session.invited(obj.stanza)

        def on_cancel():
            session.decline_invitation()

        account = obj.conn.name
146
        contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
Yann Leboulanger's avatar
Yann Leboulanger committed
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
        if contact:
            name = contact.get_shown_name()
        else:
            name = obj.jid
        pritext = _('Incoming Tictactoe')
        sectext = _('%(name)s (%(jid)s) wants to play tictactoe with you. '
            'Do you want to accept?') % {'name': name, 'jid': obj.jid}
        dialog = dialogs.NonModalConfirmationDialog(pritext, sectext=sectext,
            on_response_ok=on_ok, on_response_cancel=on_cancel)
        dialog.popup()

    @log_calls('TictactoePlugin')
    def _nec_decrypted_message_received(self, obj):
        if isinstance(obj.session, TicTacToeSession):
            obj.session.received(obj.stanza)
        game_invite = obj.stanza.getTag('invite', namespace=NS_GAMES)
        if game_invite:
            account = obj.conn.name
            game = game_invite.getTag('game')
            if game and game.getAttr('var') == NS_GAMES_TICTACTOE:
                session = obj.conn.make_new_session(obj.fjid, obj.thread_id,
                    cls=TicTacToeSession)
                self.show_request_dialog(obj, session)

class Base(object):
    def __init__(self, plugin, chat_control):
        self.plugin = plugin
        self.chat_control = chat_control
        self.contact = self.chat_control.contact
        self.account = self.chat_control.account
        self.fjid = self.contact.get_full_jid()
178
        self.add_action()
Yann Leboulanger's avatar
Yann Leboulanger committed
179 180
        self.tictactoe = None

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    def add_action(self):
        action_name = 'toggle-tictactoe-' + self.chat_control.control_id
        act = Gio.SimpleAction.new_stateful(
            action_name, None, GLib.Variant.new_boolean(False))
        act.connect('change-state', self.on_tictactoe_button_toggled)
        self.chat_control.parent_win.window.add_action(act)

        self.chat_control.control_menu.append(
            'TicTacToe', 'win.' + action_name)

    def enable_action(self, state):
        win = self.chat_control.parent_win.window
        action_name = 'toggle-tictactoe-' + self.chat_control.control_id
        win.lookup_action(action_name).set_enabled(state)

    def on_tictactoe_button_toggled(self, action, param):
Yann Leboulanger's avatar
Yann Leboulanger committed
197 198 199
        """
        Popup whiteboard
        """
200 201 202
        action.set_state(param)
        state = param.get_boolean()
        if state:
Yann Leboulanger's avatar
Yann Leboulanger committed
203 204 205 206 207 208
            if not self.tictactoe:
                self.start_tictactoe()
        else:
            self.stop_tictactoe('resign')

    def start_tictactoe(self):
209
        self.tictactoe = app.connections[self.account].make_new_session(
Yann Leboulanger's avatar
Yann Leboulanger committed
210 211 212 213 214 215
            self.fjid, cls=TicTacToeSession)
        self.tictactoe.base = self
        self.tictactoe.begin()

    def stop_tictactoe(self, reason=None):
        self.tictactoe.end_game(reason)
216 217
        if hasattr(self.tictactoe, 'board'):
            self.tictactoe.board.win.destroy()
Yann Leboulanger's avatar
Yann Leboulanger committed
218 219 220
        self.tictactoe = None

    def disconnect_from_chat_control(self):
221 222 223 224 225 226
        menu = self.chat_control.control_menu
        for i in range(menu.get_n_items()):
            label = menu.get_item_attribute_value(i, 'label')
            if label.get_string() == 'TicTacToe':
                menu.remove(i)
                break
Yann Leboulanger's avatar
Yann Leboulanger committed
227 228 229 230

class InvalidMove(Exception):
    pass

231
class TicTacToeSession(object):
Yann Leboulanger's avatar
Yann Leboulanger committed
232
    def __init__(self, conn, jid, thread_id, type_):
233 234 235 236 237 238 239 240 241 242 243 244 245 246
        self.conn = conn
        self.jid = jid
        self.type_ = type_
        self.resource = jid.getResource()

        if thread_id:
            self.received_thread_id = True
            self.thread_id = thread_id
        else:
            self.received_thread_id = False
            self.thread_id = self.generate_thread_id()

        contact = app.contacts.get_contact(
            conn.name, app.get_jid_without_resource(str(jid)))
Yann Leboulanger's avatar
Yann Leboulanger committed
247 248
        self.name = contact.get_shown_name()
        self.base = None
249
        self.control = None
250
        self.enable_encryption = False
Yann Leboulanger's avatar
Yann Leboulanger committed
251

252 253 254
    def is_loggable(self):
        return False

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    def send(self, msg):
        if self.thread_id:
            msg.NT.thread = self.thread_id

        msg.setAttr('to', self.get_to())
        self.conn.send_stanza(msg)

    def get_to(self):
        to = str(self.jid)
        return app.get_jid_without_resource(to) + '/' + self.resource

    def generate_thread_id(self):
        return ''.join(
            [f(string.ascii_letters) for f in itertools.repeat(
                random.choice, 32)]
        )

Yann Leboulanger's avatar
Yann Leboulanger committed
272
    # initiate a session
273 274 275
    def begin(self, role_s='x'):
        self.rows = self.base.plugin.config['board_size']
        self.cols = self.base.plugin.config['board_size']
Yann Leboulanger's avatar
Yann Leboulanger committed
276 277 278

        self.role_s = role_s

279
        self.strike = self.base.plugin.config['board_size']
Yann Leboulanger's avatar
Yann Leboulanger committed
280 281 282 283 284 285 286 287 288 289 290 291

        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):
292
        msg = nbxmpp.Message()
Yann Leboulanger's avatar
Yann Leboulanger committed
293 294 295 296 297 298 299 300

        invite = msg.NT.invite
        invite.setNamespace(NS_GAMES)
        invite.setAttr('type', 'new')

        game = invite.NT.game
        game.setAttr('var', NS_GAMES_TICTACTOE)

301
        x = nbxmpp.DataForm(typ='submit')
Yann Leboulanger's avatar
Yann Leboulanger committed
302 303 304 305 306
        f = x.setField('role')
        f.setType('list-single')
        f.setValue('x')
        f = x.setField('rows')
        f.setType('text-single')
307
        f.setValue(str(self.base.plugin.config['board_size']))
Yann Leboulanger's avatar
Yann Leboulanger committed
308 309
        f = x.setField('cols')
        f.setType('text-single')
310
        f.setValue(str(self.base.plugin.config['board_size']))
Yann Leboulanger's avatar
Yann Leboulanger committed
311 312
        f = x.setField('strike')
        f.setType('text-single')
313
        f.setValue(str(self.base.plugin.config['board_size']))
Yann Leboulanger's avatar
Yann Leboulanger committed
314 315 316 317 318 319 320 321 322 323

        game.addChild(node=x)

        self.send(msg)

    def read_invitation(self, msg):
        invite = msg.getTag('invite', namespace=NS_GAMES)
        game = invite.getTag('game')
        x = game.getTag('x', namespace='jabber:x:data')

324
        form = nbxmpp.DataForm(node=x)
Yann Leboulanger's avatar
Yann Leboulanger committed
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357

        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)

        # 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
358
        response = nbxmpp.Message()
Yann Leboulanger's avatar
Yann Leboulanger committed
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385

        join = response.NT.join
        join.setNamespace(NS_GAMES)

        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=NS_GAMES):
            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=NS_GAMES):
386
            app.nec.push_incoming_event(InformationEvent(None, conn=self.conn,
Yann Leboulanger's avatar
Yann Leboulanger committed
387 388 389 390 391 392 393 394
                level='info', pri_txt=_('Invitation refused'),
                sec_txt=_('%(name)s refused your invitation to play tic tac '
                'toe.') % {'name': self.name}))
            self.conn.delete_session(str(self.jid), self.thread_id)
            if self.base:
                self.base.button.set_active(False)

    def decline_invitation(self):
395
        msg = nbxmpp.Message()
Yann Leboulanger's avatar
Yann Leboulanger committed
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432

        terminate = msg.NT.decline
        terminate.setNamespace(NS_GAMES)

        self.send(msg)

    def treat_terminate(self, msg):
        term = msg.getTag('terminate', namespace=NS_GAMES)
        if term:
            if term.getAttr('reason') == 'resign':
                self.board.state = 'resign'
            self.board.win.queue_draw()
            self.received = self.game_over
            return True

    # silently ignores any received messages
    def ignore(self, msg):
        self.treat_terminate(msg)

    def game_over(self, msg):
        invite = msg.getTag('invite', namespace=NS_GAMES)

        # ignore messages unless they're renewing the game
        if invite and invite.getAttr('type') == 'renew':
            self.invited(msg)

    def wait_for_move(self, msg):
        if self.treat_terminate(msg):
            return
        turn = msg.getTag('turn', namespace=NS_GAMES)
        move = turn.getTag('move', namespace=NS_GAMES_TICTACTOE)

        row = int(move.getAttr('row'))
        col = int(move.getAttr('col'))
        id_ = int(move.getAttr('id'))

        if id_ != self.next_move_id:
433
            log.warn('unexpected move id, lost a move somewhere?')
Yann Leboulanger's avatar
Yann Leboulanger committed
434 435 436 437
            return

        try:
            self.board.mark(row, col, self.role_o)
438
        except InvalidMove as e:
Yann Leboulanger's avatar
Yann Leboulanger committed
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
            # received an invalid move, end the game.
            self.board.cheated()
            self.end_game('cheating')
            self.received = self.game_over
            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):
        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)
471 472
        except InvalidMove as e:
            log.warn('you made an invalid move')
Yann Leboulanger's avatar
Yann Leboulanger committed
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
            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):
489
        msg = nbxmpp.Message()
Yann Leboulanger's avatar
Yann Leboulanger committed
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
        msg.setType('chat')

        turn = msg.NT.turn
        turn.setNamespace(NS_GAMES)

        move = turn.NT.move
        move.setNamespace(NS_GAMES_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 end_game(self, reason):
506
        msg = nbxmpp.Message()
Yann Leboulanger's avatar
Yann Leboulanger committed
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527

        terminate = msg.NT.terminate
        terminate.setNamespace(NS_GAMES)
        terminate.setAttr('reason', reason)

        self.send(msg)

        self.received = self.game_over

    def won(self):
        self.end_game('won')
        self.board.won()

    def lost(self):
        self.end_game('lost')
        self.board.lost()

    def drawn(self):
        self.end_game('draw')
        self.board.drawn()

528 529 530 531 532 533 534 535

class DrawBoard(Gtk.DrawingArea):
    def __init__(self):
        Gtk.DrawingArea.__init__(self)
        self.set_size_request(200, 200)
        self.set_property('expand', True)


Yann Leboulanger's avatar
Yann Leboulanger committed
536 537 538 539 540 541 542 543 544
class TicTacToeBoard:
    def __init__(self, session, rows, cols):
        self.session = session

        self.state = 'None'

        self.rows = rows
        self.cols = cols

545
        self.board = [ [None] * self.cols for r in range(self.rows) ]
Yann Leboulanger's avatar
Yann Leboulanger committed
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563

        self.setup_window()

    # check if the last move (at row r and column c) won the game
    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

564
        for d in range(-strike, strike):
Yann Leboulanger's avatar
Yann Leboulanger committed
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
            r_in_range = 0 <= r+d < self.rows
            c_in_range = 0 <= c+d < self.cols

            # vertical check
            if r_in_range:
                tallyI = tallyI + 1
                if self.board[r+d][c] != p:
                    tallyI = 0

            # horizontal check
            if c_in_range:
                tally_ = tally_ + 1
                if self.board[r][c+d] != p:
                    tally_ = 0

            # diagonal checks
            if r_in_range and c_in_range:
                tallyL = tallyL + 1
                if self.board[r+d][c+d] != p:
                    tallyL = 0

            if r_in_range and 0 <= c-d < self.cols:
                tallyF = tallyF + 1
                if self.board[r+d][c-d] != p:
                    tallyF = 0

            if any([t == strike for t in (tallyL, tallyF, tallyI, tally_)]):
                return True

        return False

    # is the board full?
    def full(self):
598 599
        for r in range(self.rows):
            for c in range(self.cols):
Yann Leboulanger's avatar
Yann Leboulanger committed
600 601 602 603 604 605
                if self.board[r][c] == None:
                    return False

        return True

    def setup_window(self):
606 607 608
        self.win = Gtk.Window()
        draw = DrawBoard()
        self.win.add(draw)
Yann Leboulanger's avatar
Yann Leboulanger committed
609 610 611 612

        self.title_prefix = 'tic-tac-toe with %s' % self.session.name
        self.set_title()

613
        self.win.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
Yann Leboulanger's avatar
Yann Leboulanger committed
614
        self.win.connect('button-press-event', self.clicked)
615 616

        draw.connect('draw', self.do_draw)
Yann Leboulanger's avatar
Yann Leboulanger committed
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635

        self.win.show_all()

    def clicked(self, widget, event):
        if not self.session.is_my_turn():
            return

        (width, height) = 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
636
    def do_draw(self, widget, cr):
Yann Leboulanger's avatar
Yann Leboulanger committed
637 638
        cr.set_source_rgb(1.0, 1.0, 1.0)

639 640
        layout = PangoCairo.create_layout(cr)
        text_height = layout.get_pixel_extents()[1].height
Yann Leboulanger's avatar
Yann Leboulanger committed
641

642
        (width, height) = self.win.get_size()
Yann Leboulanger's avatar
Yann Leboulanger committed
643 644

        row_height = (height - text_height) // self.rows
645
        col_width  = width // self.cols
Yann Leboulanger's avatar
Yann Leboulanger committed
646 647 648

        cr.set_source_rgb(0, 0, 0)
        cr.set_line_width(2)
649
        for x in range(1, self.cols):
Yann Leboulanger's avatar
Yann Leboulanger committed
650 651
            cr.move_to(col_width * x, 0)
            cr.line_to(col_width * x, height - text_height)
652
        for x in range(1, self.rows):
Yann Leboulanger's avatar
Yann Leboulanger committed
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
            cr.move_to(0, row_height * x)
            cr.line_to(width, row_height * x)
        cr.stroke()

        cr.move_to(0, height - text_height)
        if self.state == 'None':
            if self.session.is_my_turn():
                txt = _('It\'s your turn')
            else:
                txt = _('It\'s %(name)s\'s turn') % {'name': self.session.name}
        elif self.state == 'won':
            txt = _('You won !')
        elif self.state == 'lost':
            txt = _('You lost !')
        elif self.state == 'resign': # other part resigned
            txt = _('%(name)s capitulated') % {'name': self.session.name}
        elif self.state == 'cheated': # other part cheated
            txt = _('%(name)s cheated') % {'name': self.session.name}
        else: #draw
            txt = _('It\'s a draw')
673 674 675 676
        layout.set_text(txt, -1)
        # Inform Pango to re-layout the text with the new transformation
        PangoCairo.update_layout(cr, layout)
        PangoCairo.show_layout(cr, layout)
Yann Leboulanger's avatar
Yann Leboulanger committed
677

678 679
        for i in range(self.rows):
            for j in range(self.cols):
Yann Leboulanger's avatar
Yann Leboulanger committed
680 681 682 683 684 685 686
                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)

    def draw_x(self, cr, row, col, row_height, col_width):
        if self.session.role_s == 'x':
687
            color = '#3d79fb'  # out
Yann Leboulanger's avatar
Yann Leboulanger committed
688
        else:
689
            color = '#f03838'  # red
690 691 692
        rgba = Gdk.RGBA()
        rgba.parse(color)
        cr.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha)
Yann Leboulanger's avatar
Yann Leboulanger committed
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711

        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):
        if self.session.role_s == 'o':
712
            color = '#3d79fb'  # out
Yann Leboulanger's avatar
Yann Leboulanger committed
713
        else:
714
            color = '#f03838'  # red
715 716 717
        rgba = Gdk.RGBA()
        rgba.parse(color)
        cr.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha)
Yann Leboulanger's avatar
Yann Leboulanger committed
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761

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

    def cheated(self):
        self.state == 'cheated'
        self.win.queue_draw()
762 763 764 765 766 767


class TictactoePluginConfigDialog(GajimPluginConfigDialog):
    def init(self):
        self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
            'config_dialog.ui')
768
        self.xml = Gtk.Builder()
769 770 771
        self.xml.set_translation_domain('gajim_plugins')
        self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH, ['vbox1'])
        self.board_size_spinbutton = self.xml.get_object('board_size')
772
        self.board_size_spinbutton.get_adjustment().configure(3, 3, 10, 1, 1, 0)
773
        vbox = self.xml.get_object('vbox1')
774
        self.get_child().pack_start(vbox, True, True, 0)
775 776 777 778 779 780 781 782

        self.xml.connect_signals(self)

    def on_run(self):
        self.board_size_spinbutton.set_value(self.plugin.config['board_size'])

    def board_size_value_changed(self, spinbutton):
        self.plugin.config['board_size'] = int(spinbutton.get_value())