-
Philipp Hörist authoredPhilipp Hörist authored
plugin.py 23.19 KiB
#
# Copyright (C) 2011 Yann Leboulanger <asterix AT lagaule.org>
#
# This file is part of the TicTacToe plugin for 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
'''
import string
import itertools
import random
from functools import partial
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Gio
from gi.repository import GLib
import nbxmpp
from gajim import chat_control
from gajim.common import app
from gajim.common import ged
from gajim.common import helpers
from gajim.common.connection_handlers_events import InformationEvent
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import NewConfirmationDialog
from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import log
from gajim.plugins.helpers import log_calls
from gajim.plugins.plugins_i18n import _
from tictactoe.config_dialog import TicTacToeConfigDialog
try:
import gi
gi.require_version('PangoCairo', '1.0')
from gi.repository import PangoCairo
HAS_PANGOCAIRO = True
except ImportError:
HAS_PANGOCAIRO = False
NS_GAMES = 'http://jabber.org/protocol/games'
NS_GAMES_TICTACTOE = NS_GAMES + '/tictactoe'
class TictactoePlugin(GajimPlugin):
@log_calls('TictactoePlugin')
def init(self):
if not HAS_PANGOCAIRO:
self.activatable = False
self.config_dialog = None
self.available_text = _('TicTacToe requires PangoCairo to run')
self.description = _('Play Tictactoe.')
self.config_dialog = partial(TicTacToeConfigDialog, self)
self.events_handlers = {
'decrypted-message-received': (
ged.PREGUI, self._nec_decrypted_message_received),
}
self.gui_extension_points = {
'chat_control': (self.connect_with_chat_control,
self.disconnect_from_chat_control),
'chat_control_base_update_toolbar': (
self.update_button_state, None),
'update_caps': (self._update_caps, None),
}
self.config_default_values = {
'board_size': (5, ''),
}
self.controls = []
self.announce_caps = True
@log_calls('TictactoePlugin')
def _update_caps(self, account, features):
if not self.announce_caps:
return
features.append(NS_GAMES)
features.append(NS_GAMES_TICTACTOE)
@log_calls('TictactoePlugin')
def activate(self):
self.announce_caps = True
for con in app.connections.values():
con.get_module('Caps').update_caps()
@log_calls('TictactoePlugin')
def deactivate(self):
self.announce_caps = False
for con in app.connections.values():
con.get_module('Caps').update_caps()
@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?
conn = app.connections[control.account]
sessions = conn.get_sessions(control.contact.jid)
tictactoes = [s for s in sessions if isinstance(
s, TicTacToeSession)]
if tictactoes:
base.tictactoe = tictactoes[0]
base.enable_action(True)
@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)):
base.enable_action(True)
else:
base.enable_action(False)
@log_calls('TictactoePlugin')
def show_request_dialog(self, obj, session):
def _on_accept():
session.invited(obj.stanza)
def _on_decline():
session.decline_invitation()
account = obj.conn.name
contact = app.contacts.get_first_contact_from_jid(account, obj.jid)
if contact:
name = contact.get_shown_name()
else:
name = obj.jid
NewConfirmationDialog(
_('Incoming Tictactoe'),
_('Incoming Tictactoe Invitation'),
_('%(name)s (%(jid)s) wants to play tictactoe with you.') % {
'name': name, 'jid': obj.jid},
[DialogButton.make('Cancel',
text=_('_Decline'),
callback=_on_decline),
DialogButton.make('OK',
text=_('_Accept'),
callback=_on_accept)],
modal=False,
transient_for=app.app.get_active_window()).show()
@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:
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()
self.add_action()
self.tictactoe = None
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):
"""
Popup whiteboard
"""
action.set_state(param)
state = param.get_boolean()
if state:
if not self.tictactoe:
self.start_tictactoe()
else:
self.stop_tictactoe('resign')
def start_tictactoe(self):
self.tictactoe = app.connections[self.account].make_new_session(
self.fjid, cls=TicTacToeSession)
self.tictactoe.base = self
self.tictactoe.begin()
def stop_tictactoe(self, reason=None):
self.tictactoe.end_game(reason)
if hasattr(self.tictactoe, 'board'):
self.tictactoe.board.win.destroy()
self.tictactoe = None
def disconnect_from_chat_control(self):
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
class InvalidMove(Exception):
pass
class TicTacToeSession(object):
def __init__(self, conn, jid, thread_id, type_):
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)))
self.name = contact.get_shown_name()
self.base = None
self.control = None
self.enable_encryption = False
def is_loggable(self):
return False
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)
jid = app.get_jid_without_resource(to)
if self.resource:
jid += '/' + self.resource
return jid
def generate_thread_id(self):
return ''.join(
[f(string.ascii_letters) for f in itertools.repeat(
random.choice, 32)]
)
# Initiate a session
def begin(self, role_s='x'):
self.rows = self.base.plugin.config['board_size']
self.cols = self.base.plugin.config['board_size']
self.role_s = role_s
self.strike = self.base.plugin.config['board_size']
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 = nbxmpp.Message()
invite = msg.NT.invite
invite.setNamespace(NS_GAMES)
invite.setAttr('type', 'new')
game = invite.NT.game
game.setAttr('var', NS_GAMES_TICTACTOE)
x = nbxmpp.DataForm(typ='submit')
f = x.setField('role')
f.setType('list-single')
f.setValue('x')
f = x.setField('rows')
f.setType('text-single')
f.setValue(str(self.base.plugin.config['board_size']))
f = x.setField('cols')
f.setType('text-single')
f.setValue(str(self.base.plugin.config['board_size']))
f = x.setField('strike')
f.setType('text-single')
f.setValue(str(self.base.plugin.config['board_size']))
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')
form = nbxmpp.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)
# 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 = nbxmpp.Message()
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):
app.nec.push_incoming_event(
InformationEvent(
None,
conn=self.conn,
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)
def decline_invitation(self):
msg = nbxmpp.Message()
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:
log.warn('unexpected move id, lost a move somewhere?')
return
try:
self.board.mark(row, col, self.role_o)
except InvalidMove:
# 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)
except InvalidMove:
log.warn('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 = nbxmpp.Message()
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):
msg = nbxmpp.Message()
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()
class DrawBoard(Gtk.DrawingArea):
def __init__(self):
Gtk.DrawingArea.__init__(self)
self.set_size_request(200, 200)
self.set_property('expand', True)
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 range(self.rows)]
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
for d in range(-strike, strike):
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):
for r in range(self.rows):
for c in range(self.cols):
if self.board[r][c] is None:
return False
return True
def setup_window(self):
self.win = Gtk.Window()
draw = DrawBoard()
self.win.add(draw)
self.title_prefix = 'tic-tac-toe with %s' % self.session.name
self.set_title()
self.win.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.win.connect('button-press-event', self.clicked)
draw.connect('draw', self.do_draw)
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
def do_draw(self, widget, cr):
cr.set_source_rgb(1.0, 1.0, 1.0)
layout = PangoCairo.create_layout(cr)
text_height = layout.get_pixel_extents()[1].height
(width, height) = self.win.get_size()
row_height = (height - text_height) // self.rows
col_width = width // self.cols
cr.set_source_rgb(0, 0, 0)
cr.set_line_width(2)
for x in range(1, self.cols):
cr.move_to(col_width * x, 0)
cr.line_to(col_width * x, height - text_height)
for x in range(1, self.rows):
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')
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)
for i in range(self.rows):
for j in range(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)
def draw_x(self, cr, row, col, row_height, col_width):
if self.session.role_s == 'x':
color = '#3d79fb' # Out
else:
color = '#f03838' # Red
rgba = Gdk.RGBA()
rgba.parse(color)
cr.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha)
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':
color = '#3d79fb' # out
else:
color = '#f03838' # red
rgba = Gdk.RGBA()
rgba.parse(color)
cr.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha)
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()