21 Commits

Author SHA1 Message Date
9234e3858f add italian translations (big thanks to Carola and nick!) 2016-05-22 23:32:43 +02:00
ba47f4c19e final version? 2016-05-22 19:21:51 +02:00
4cdffffa5f Optional multi-translations 2016-05-22 17:02:27 +02:00
005445c4dd add database file to gitignore 2016-05-22 14:47:26 +02:00
6c610c1aeb settings UI added, save locale to database 2016-05-22 14:45:51 +02:00
cddf13dc5d add __ function to translate complete stack, add dummy decorators to pull locales from db 2016-05-22 03:13:05 +02:00
5ece46527a locales are working, added de_DE locale 2016-05-21 21:41:38 +02:00
becc7e28dc fix broken method call 2016-05-21 18:56:58 +02:00
a02813477a more stable first-card drawing 2016-05-21 18:56:27 +02:00
73365f49fc draw method reset draw counter on empty deck 2016-05-21 18:55:53 +02:00
d5b76c5c12 fix result list generation 2016-05-21 18:55:03 +02:00
a39fa85b3b more translation supprt 2016-05-20 18:35:21 +02:00
aee310ec9c handle empty decks on player join 2016-05-20 18:34:27 +02:00
0dcd1f6cdc use regular formatting 2016-05-20 17:55:08 +02:00
2316ab8a1c pot formatting 2016-05-20 17:53:01 +02:00
8af8852d05 initial translation support 2016-05-19 23:18:05 +02:00
204b057810 add encoding 2016-05-19 23:15:46 +02:00
9936d97373 update test_end_game 2016-05-19 21:29:07 +02:00
424219d825 fix end_game 2016-05-19 21:28:04 +02:00
c9f7c09a46 update readme 2016-05-19 20:57:32 +02:00
6204868a18 separate game logic from bot interface,
introduce exceptions instead of boolean returns,
remove repetitive code,
begin unit tests,
improve docstrings,
update to python-telegram-bot==4.1.1,
add ponyorm settings classes (unused)
2016-05-19 20:56:52 +02:00
27 changed files with 2735 additions and 987 deletions

5
.gitignore vendored
View File

@ -47,7 +47,7 @@ coverage.xml
# Translations # Translations
*.mo *.mo
*.pot # *.pot
# Django stuff: # Django stuff:
*.log *.log
@ -63,3 +63,6 @@ target/
# PyCharm # PyCharm
.idea .idea
# Database file
uno.sqlite3

332
ISMCTS.py
View File

@ -1,332 +0,0 @@
# This is a very simple Python 2.7 implementation of the Information Set Monte Carlo Tree Search algorithm.
# The function ISMCTS(rootstate, itermax, verbose = False) is towards the bottom of the code.
# It aims to have the clearest and simplest possible code, and for the sake of clarity, the code
# is orders of magnitude less efficient than it could be made, particularly by using a
# state.GetRandomMove() or state.DoRandomRollout() function.
#
# An example GameState classes for Knockout Whist is included to give some idea of how you
# can write your own GameState to use ISMCTS in your hidden information game.
#
# Written by Peter Cowling, Edward Powley, Daniel Whitehouse (University of York, UK) September 2012 - August 2013.
#
# Licence is granted to freely use and distribute for any sensible/legal purpose so long as this comment
# remains in any distributed code.
#
# For more information about Monte Carlo Tree Search check out our web site at www.mcts.ai
# Also read the article accompanying this code at ***URL HERE***
from math import *
import random, sys
from game import Game as UNOGame
from player import Player as UNOPlayer
from utils import list_subtract_unsorted
import card as c
class GameState:
""" A state of the game, i.e. the game board. These are the only functions which are
absolutely necessary to implement ISMCTS in any imperfect information game,
although they could be enhanced and made quicker, for example by using a
GetRandomMove() function to generate a random move during rollout.
By convention the players are numbered 1, 2, ..., self.numberOfPlayers.
"""
def __init__(self):
pass
def GetNextPlayer(self, p):
""" Return the player to the left of the specified player
"""
raise NotImplementedError()
def Clone(self):
""" Create a deep clone of this game state.
"""
raise NotImplementedError()
def CloneAndRandomize(self, observer):
""" Create a deep clone of this game state, randomizing any information not visible to the specified observer player.
"""
raise NotImplementedError()
def DoMove(self, move):
""" Update a state by carrying out the given move.
Must update playerToMove.
"""
raise NotImplementedError()
def GetMoves(self):
""" Get all possible moves from this state.
"""
raise NotImplementedError()
def GetResult(self, player):
""" Get the game result from the viewpoint of player.
"""
raise NotImplementedError()
def __repr__(self):
""" Don't need this - but good style.
"""
pass
class UNOState(GameState):
""" A state of the game UNO.
"""
def __init__(self, game):
""" Initialise the game state. n is the number of players (from 2 to 7).
"""
self.game = game
@property
def playerToMove(self):
return self.game.current_player
@property
def numberOfPlayers(self):
return len(self.game.players)
def CloneAndRandomize(self, observer):
""" Create a deep clone of this game state.
"""
game = UNOGame(None)
game.deck.cards.append(game.last_card)
game.draw_counter = self.game.draw_counter
game.last_card = self.game.last_card
game.deck.cards = list_subtract_unsorted(game.deck.cards,
self.game.deck.graveyard)
game.deck.graveyard = list(self.game.deck.graveyard)
for player in self.game.players:
p = UNOPlayer(game, None)
if player is observer:
p.cards = list(player.cards)
else:
for i in range(len(player.cards)):
p.cards.append(game.deck.draw())
return UNOState(game)
def DoMove(self, move):
""" Update a state by carrying out the given move.
Must update playerToMove.
"""
if move == 'draw':
for n in range(self.game.draw_counter or 1):
self.game.current_player.cards.append(
self.game.deck.draw()
)
self.game.draw_counter = 0
self.game.turn()
else:
self.game.current_player.cards.remove(move)
self.game.play_card(move)
if move.special:
self.game.turn()
self.game.choosing_color = False
def GetMoves(self):
""" Get all possible moves from this state.
"""
if self.game.current_player.cards:
playable = self.game.current_player.playable_cards()
playable_converted = list()
for card in playable:
if not card.color:
for color in c.COLORS:
playable_converted.append(
c.Card(color, None, card.special)
)
else:
playable_converted.append(card)
# playable_converted.append('draw')
return playable_converted or ['draw']
else:
return list()
def GetResult(self, player):
""" Get the game result from the viewpoint of player.
"""
return 1 if not player.cards else 0
def __repr__(self):
""" Return a human-readable representation of the state
"""
return '\n'.join(
['%s: %s' % (p.user, [str(c) for c in p.cards])
for p in self.game.players]
) + "\nDeck: %s" % str([str(crd) for crd in self.game.deck.cards]) \
+ "\nGrav: %s" % str([str(crd) for crd in self.game.deck.graveyard])
class Node:
""" A node in the game tree. Note wins is always from the viewpoint of playerJustMoved.
"""
def __init__(self, move=None, parent=None, playerJustMoved=None):
self.move = move # the move that got us to this node - "None" for the root node
self.parentNode = parent # "None" for the root node
self.childNodes = []
self.wins = 0
self.visits = 0
self.avails = 1
self.playerJustMoved = playerJustMoved # the only part of the state that the Node needs later
def GetUntriedMoves(self, legalMoves):
""" Return the elements of legalMoves for which this node does not have children.
"""
# Find all moves for which this node *does* have children
triedMoves = [child.move for child in self.childNodes]
# Return all moves that are legal but have not been tried yet
return [move for move in legalMoves if move not in triedMoves]
def UCBSelectChild(self, legalMoves, exploration=0.7):
""" Use the UCB1 formula to select a child node, filtered by the given list of legal moves.
exploration is a constant balancing between exploitation and exploration, with default value 0.7 (approximately sqrt(2) / 2)
"""
# Filter the list of children by the list of legal moves
legalChildren = [child for child in self.childNodes if
child.move in legalMoves]
# Get the child with the highest UCB score
s = max(legalChildren, key=lambda c: float(c.wins) / float(
c.visits) + exploration * sqrt(log(c.avails) / float(c.visits)))
# Update availability counts -- it is easier to do this now than during backpropagation
for child in legalChildren:
child.avails += 1
# Return the child selected above
return s
def AddChild(self, m, p):
""" Add a new child node for the move m.
Return the added child node
"""
n = Node(move=m, parent=self, playerJustMoved=p)
self.childNodes.append(n)
return n
def Update(self, terminalState):
""" Update this node - increment the visit count by one, and increase the win count by the result of terminalState for self.playerJustMoved.
"""
self.visits += 1
if self.playerJustMoved is not None:
self.wins += terminalState.GetResult(self.playerJustMoved)
def __repr__(self):
return "[M:%s W/V/A: %4i/%4i/%4i]" % (
self.move, self.wins, self.visits, self.avails)
def TreeToString(self, indent):
""" Represent the tree as a string, for debugging purposes.
"""
s = self.IndentString(indent) + str(self)
for c in self.childNodes:
s += c.TreeToString(indent + 1)
return s
def IndentString(self, indent):
s = "\n"
for i in range(1, indent + 1):
s += "| "
return s
def ChildrenToString(self):
s = ""
for c in self.childNodes:
s += str(c) + "\n"
return s
def ISMCTS(rootstate, itermax, verbose=False):
""" Conduct an ISMCTS search for itermax iterations starting from rootstate.
Return the best move from the rootstate.
"""
rootnode = Node()
for i in range(itermax):
node = rootnode
# Determinize
state = rootstate.CloneAndRandomize(rootstate.playerToMove)
# Select
while state.GetMoves() != [] and node.GetUntriedMoves(
state.GetMoves()) == []: # node is fully expanded and non-terminal
node = node.UCBSelectChild(state.GetMoves())
state.DoMove(node.move)
# Expand
untriedMoves = node.GetUntriedMoves(state.GetMoves())
if untriedMoves != []: # if we can expand (i.e. state/node is non-terminal)
m = random.choice(untriedMoves)
player = state.playerToMove
state.DoMove(m)
node = node.AddChild(m, player) # add child and descend tree
# Simulate
while state.GetMoves() != []: # while state is non-terminal
state.DoMove(random.choice(state.GetMoves()))
# Backpropagate
while node != None: # backpropagate from the expanded node and work back to the root node
node.Update(state)
node = node.parentNode
# Output some information about the tree - can be omitted
if (verbose):
print(rootnode.TreeToString(0))
else:
print(rootnode.ChildrenToString())
return max(rootnode.childNodes, key=lambda
c: c.visits).move # return the move that was most visited
def PlayGame():
""" Play a sample game between two ISMCTS players.
*** This is only a demo and not used by the actual bot ***
"""
game = UNOGame(None)
me = UNOPlayer(game, "Player 1")
UNOPlayer(game, "Player 2")
UNOPlayer(game, "Player 3")
UNOPlayer(game, "Player 4")
UNOPlayer(game, "Player 5")
state = UNOState(game)
while (state.GetMoves() != []):
print(str(state))
# Use different numbers of iterations (simulations, tree nodes) for different players
m = ISMCTS(rootstate=state, itermax=10, verbose=False)
# if state.playerToMove is me:
# m = ISMCTS(rootstate=state, itermax=1000, verbose=False)
# else:
# m = ISMCTS(rootstate=state, itermax=100, verbose=False)
print("Best Move: " + str(m) + "\n")
state.DoMove(m)
someoneWon = False
for p in game.players:
if state.GetResult(p) > 0:
print("Player " + str(p) + " wins!")
someoneWon = True
if not someoneWon:
print("Nobody wins!")
if __name__ == "__main__":
PlayGame()

View File

@ -3,7 +3,7 @@ Telegram Bot that allows you to play the popular card game UNO via inline querie
To run the bot yourself, you will need: To run the bot yourself, you will need:
- Python (tested with 3.4 and 3.5) - Python (tested with 3.4 and 3.5)
- The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module version 4.0.3 - The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module version 4.1.1
Get a bot token from [@BotFather](http://telegram.me/BotFather), place it in `credentials.py` and run the bot with `python3 bot.py` Get a bot token from [@BotFather](http://telegram.me/BotFather), place it in `credentials.py` and run the bot with `python3 bot.py`

10
TRANSLATORS.md Normal file
View File

@ -0,0 +1,10 @@
# Translators
The following awesome people contributed to this project by translating it:
| Locale | Translators |
|--------|--------------------------------------------------------|
| de_DE | [Jannes Höke](https://github.com/jh0ker) |
| it_IT | Carola Mariano, nick |
Please add yourself here alphabetically when you submit your first translation.

834
bot.py

File diff suppressed because it is too large Load Diff

31
card.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -114,15 +115,7 @@ STICKERS = {
'y_skip': 'BQADBAADQwIAAl9XmQABO_AZKtxY6IMC', 'y_skip': 'BQADBAADQwIAAl9XmQABO_AZKtxY6IMC',
'y_reverse': 'BQADBAADQQIAAl9XmQABZdQFahGG6UQC', 'y_reverse': 'BQADBAADQQIAAl9XmQABZdQFahGG6UQC',
'draw_four': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC', 'draw_four': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_r': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_b': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_g': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_y': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'colorchooser': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C', 'colorchooser': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_r': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_b': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_g': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_y': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'option_draw': 'BQADBAADzAIAAl9XmQABTkPaOqA5HIMC', 'option_draw': 'BQADBAADzAIAAl9XmQABTkPaOqA5HIMC',
'option_pass': 'BQADBAADzgIAAl9XmQABWSDq3RIg3c0C', 'option_pass': 'BQADBAADzgIAAl9XmQABWSDq3RIg3c0C',
'option_bluff': 'BQADBAADygIAAl9XmQABJoLfB9ntI2UC', 'option_bluff': 'BQADBAADygIAAl9XmQABJoLfB9ntI2UC',
@ -188,9 +181,7 @@ STICKERS_GREY = {
class Card(object): class Card(object):
""" """This class represents an UNO card"""
This class represents a card.
"""
def __init__(self, color, value, special=None): def __init__(self, color, value, special=None):
self.color = color self.color = color
@ -199,9 +190,6 @@ class Card(object):
def __str__(self): def __str__(self):
if self.special: if self.special:
if self.color:
return '%s_%s' % (self.special, self.color)
else:
return self.special return self.special
else: else:
return '%s_%s' % (self.color, self.value) return '%s_%s' % (self.color, self.value)
@ -216,23 +204,16 @@ class Card(object):
return '%s%s' % (COLOR_ICONS[self.color], self.value.capitalize()) return '%s%s' % (COLOR_ICONS[self.color], self.value.capitalize())
def __eq__(self, other): def __eq__(self, other):
""" Needed for sorting the cards """ """Needed for sorting the cards"""
s1 = str(self) return str(self) == str(other)
s2 = str(other)
return (s1 == s2
if not self.special else
s1 == s2 or
s1[:-2] == s2[:-2] or
s1[:-2] == s2 or
s1 == s2[:-2])
def __lt__(self, other): def __lt__(self, other):
""" Needed for sorting the cards """ """Needed for sorting the cards"""
return str(self) < str(other) return str(self) < str(other)
def from_str(string): def from_str(string):
""" Decode a Card object from a string """ """Decodes a Card object from a string"""
if string not in SPECIALS: if string not in SPECIALS:
color, value = string.split('_') color, value = string.split('_')
return Card(color, value) return Card(color, value)

21
chat_setting.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
pass

24
database.py Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pony.orm import Database, db_session, Optional, Required, Set, PrimaryKey
# Database singleton
db = Database()

18
deck.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -18,9 +19,11 @@
from random import shuffle from random import shuffle
import logging
import card as c import card as c
from card import Card from card import Card
import logging from errors import DeckEmptyError
class Deck(object): class Deck(object):
@ -45,26 +48,25 @@ class Deck(object):
self.shuffle() self.shuffle()
def shuffle(self): def shuffle(self):
""" Shuffle the deck """ """Shuffles the deck"""
self.logger.debug("Shuffling Deck") self.logger.debug("Shuffling Deck")
shuffle(self.cards) shuffle(self.cards)
def draw(self): def draw(self):
""" Draw a card from this deck """ """Draws a card from this deck"""
try: try:
card = self.cards.pop() card = self.cards.pop()
if card.special:
card = Card(None, None, card.special)
self.logger.debug("Drawing card " + str(card)) self.logger.debug("Drawing card " + str(card))
return card return card
except IndexError: except IndexError:
if len(self.graveyard):
while len(self.graveyard): while len(self.graveyard):
self.cards.append(self.graveyard.pop()) self.cards.append(self.graveyard.pop())
self.shuffle() self.shuffle()
return self.draw() return self.draw()
else:
raise DeckEmptyError()
def dismiss(self, card): def dismiss(self, card):
""" All played cards should be returned into the deck """ """Returns a card to the deck"""
# if card.special:
# card.color = None
self.graveyard.append(card) self.graveyard.append(card)

38
errors.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class NoGameInChatError(Exception):
pass
class AlreadyJoinedError(Exception):
pass
class LobbyClosedError(Exception):
pass
class NotEnoughPlayersError(Exception):
pass
class DeckEmptyError(Exception):
pass

26
game.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -33,21 +34,22 @@ class Game(object):
started = False started = False
owner = None owner = None
open = True open = True
translate = False
players_won = 0
def __init__(self, chat): def __init__(self, chat):
self.chat = chat self.chat = chat
self.deck = Deck() self.last_card = None
self.last_card = self.deck.draw()
while self.last_card.special: while not self.last_card or self.last_card.special:
self.deck.cards.append(self.last_card) self.deck = Deck()
self.deck.shuffle()
self.last_card = self.deck.draw() self.last_card = self.deck.draw()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@property @property
def players(self): def players(self):
"""Returns a list of all players in this game"""
players = list() players = list()
if not self.current_player: if not self.current_player:
return players return players
@ -61,18 +63,23 @@ class Game(object):
return players return players
def reverse(self): def reverse(self):
""" Reverse the direction of play """ """Reverses the direction of game"""
self.reversed = not self.reversed self.reversed = not self.reversed
def turn(self): def turn(self):
""" Mark the turn as over and change the current player """ """Marks the turn as over and change the current player"""
self.logger.debug("Next Player") self.logger.debug("Next Player")
self.current_player = self.current_player.next self.current_player = self.current_player.next
self.current_player.drew = False self.current_player.drew = False
self.current_player.turn_started = datetime.now() self.current_player.turn_started = datetime.now()
self.choosing_color = False
def play_card(self, card): def play_card(self, card):
""" Play a card and trigger its effects """ """
Plays a card and triggers its effects.
Should be called only from Player.play or on game start to play the
first card
"""
self.deck.dismiss(self.last_card) self.deck.dismiss(self.last_card)
self.last_card = card self.last_card = card
@ -100,7 +107,6 @@ class Game(object):
self.choosing_color = True self.choosing_color = True
def choose_color(self, color): def choose_color(self, color):
""" Carries out the color choosing and turns the game """ """Carries out the color choosing and turns the game"""
self.last_card.color = color self.last_card.color = color
self.turn() self.turn()
self.choosing_color = False

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -21,6 +22,8 @@ import logging
from game import Game from game import Game
from player import Player from player import Player
from errors import (AlreadyJoinedError, LobbyClosedError, NoGameInChatError,
NotEnoughPlayersError)
class GameManager(object): class GameManager(object):
@ -38,7 +41,7 @@ class GameManager(object):
""" """
chat_id = chat.id chat_id = chat.id
self.logger.info("Creating new game with id " + str(chat_id)) self.logger.debug("Creating new game in chat " + str(chat_id))
game = Game(chat) game = Game(chat)
if chat_id not in self.chatid_games: if chat_id not in self.chatid_games:
@ -47,13 +50,17 @@ class GameManager(object):
self.chatid_games[chat_id].append(game) self.chatid_games[chat_id].append(game)
return game return game
def join_game(self, chat_id, user): def join_game(self, user, chat):
""" Create a player from the Telegram user and add it to the game """ """ Create a player from the Telegram user and add it to the game """
self.logger.info("Joining game with id " + str(chat_id)) self.logger.info("Joining game with id " + str(chat.id))
try: try:
game = self.chatid_games[chat_id][-1] game = self.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
return None raise NoGameInChatError()
if not game.open:
raise LobbyClosedError()
if user.id not in self.userid_players: if user.id not in self.userid_players:
self.userid_players[user.id] = list() self.userid_players[user.id] = list()
@ -61,28 +68,37 @@ class GameManager(object):
players = self.userid_players[user.id] players = self.userid_players[user.id]
# Don not re-add a player and remove the player from previous games in # Don not re-add a player and remove the player from previous games in
# this chat # this chat, if he is in one of them
for player in players: for player in players:
if player in game.players: if player in game.players:
return False raise AlreadyJoinedError()
else: else:
self.leave_game(user, chat_id) try:
self.leave_game(user, chat)
except NoGameInChatError:
pass
except NotEnoughPlayersError:
self.end_game(chat, user)
player = Player(game, user) player = Player(game, user)
players.append(player) players.append(player)
self.userid_current[user.id] = player self.userid_current[user.id] = player
return True
def leave_game(self, user, chat_id): def leave_game(self, user, chat):
""" Remove a player from its current game """ """ Remove a player from its current game """
try:
players = self.userid_players[user.id]
games = self.chatid_games[chat_id]
for player in players: player = self.player_for_user_in_chat(user, chat)
for game in games: players = self.userid_players.get(user.id, list())
if player in game.players:
if not player:
raise NoGameInChatError
game = player.game
if len(game.players) < 3:
raise NotEnoughPlayersError()
if player is game.current_player: if player is game.current_player:
game.turn() game.turn()
@ -90,49 +106,47 @@ class GameManager(object):
players.remove(player) players.remove(player)
# If this is the selected game, switch to another # If this is the selected game, switch to another
if self.userid_current[user.id] is player: if self.userid_current.get(user.id, None) is player:
if len(players): if players:
self.userid_current[user.id] = players[0] self.userid_current[user.id] = players[0]
else: else:
del self.userid_current[user.id] del self.userid_current[user.id]
return True del self.userid_players[user.id]
else:
return False
except KeyError: def end_game(self, chat, user):
return False
def end_game(self, chat_id, user):
""" """
End a game End a game
""" """
self.logger.info("Game in chat " + str(chat_id) + " ended") self.logger.info("Game in chat " + str(chat.id) + " ended")
players = self.userid_players[user.id]
games = self.chatid_games[chat_id]
the_game = None
# Find the correct game instance to end # Find the correct game instance to end
for player in players: player = self.player_for_user_in_chat(user, chat)
for game in games:
if player in game.players:
the_game = game
break
if the_game:
break
else:
return
for player in the_game.players: if not player:
if player.ai: raise NoGameInChatError
continue
this_users_players = self.userid_players[player.user.id] game = player.game
this_users_players.remove(player)
if len(this_users_players) is 0: # Clear game
del self.userid_players[player.user.id] for player_in_game in game.players:
del self.userid_current[player.user.id] this_users_players = self.userid_players[player_in_game.user.id]
else: this_users_players.remove(player_in_game)
if this_users_players:
self.userid_current[player.user.id] = this_users_players[0] self.userid_current[player.user.id] = this_users_players[0]
else:
del self.userid_players[player_in_game.user.id]
del self.userid_current[player_in_game.user.id]
self.chatid_games[chat_id].remove(the_game) self.chatid_games[chat.id].remove(game)
return if not self.chatid_games[chat.id]:
del self.chatid_games[chat.id]
def player_for_user_in_chat(self, user, chat):
players = self.userid_players.get(user.id, list())
for player in players:
if player.game.chat.id == chat.id:
return player
else:
return None

0
locales/__init__.py Normal file
View File

View File

@ -0,0 +1,442 @@
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Jannes Höke <uno@jhoeke.de>, 2016.
#
#: bot.py:224
msgid ""
msgstr ""
"Project-Id-Version: uno_bot 0.1\n"
"Report-Msgid-Bugs-To: uno@jhoeke.de\n"
"POT-Creation-Date: 2016-05-19 22:38+0200\n"
"PO-Revision-Date: 2016-05-21 21:16+0200\n"
"Last-Translator: Jannes Höke <uno@jhoeke.de>\n"
"Language-Team: Deutsch <uno@jhoeke.de>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Gtranslator 2.91.6\n"
#: bot.py:60
msgid "Follow these steps:\n"
"\n"
"1. Add this bot to a group\n"
"2. In the group, start a new game with /new or join an already running game "
"with /join\n"
"3. After at least two players have joined, start the game with /start\n"
"4. Type <code>@mau_mau_bot</code> into your chat box and hit <b>space</b>, "
"or click the <code>via @mau_mau_bot</code> text next to messages. You will "
"see your cards (some greyed out), any extra options like drawing, and a <b>?"
"</b> to see the current game state. The <b>greyed out cards</b> are those "
"you <b>can not play</b> at the moment. Tap an option to execute the selected "
"action.\n"
"Players can join the game at any time. To leave a game, use /leave. If a "
"player takes more than 90 seconds to play, you can use /skip to skip that "
"player.\n"
"\n"
"<b>Language</b> and other settings: /settings\n"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\n"
"/enable_translations - Translate relevant texts into all "
"languages spoken in a game\n"
"/disable_translations - Use English for those texts\n"
"\n"
"<b>Experimental:</b> Play in multiple groups at the same time. Press the "
"<code>Current game: ...</code> button and select the group you want to play "
"a card in.\n"
"If you enjoy this bot, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">rate me</a>, join the <a href=\"https://telegram.me/"
"unobotupdates\">update channel</a> and buy an UNO card game."
msgstr ""
"Folge den folgenden Schritten:\n"
"\n"
"1. Füge diesen Bot einer Gruppe hinzu\n"
"2. In einer Gruppe kannst du mit /new ein neues Spiel erstellen und mit /"
"join einem bestehenden Spiel beitreten\n"
"3. Nachdem mindestens zwei Spieler beigetreten sind, starte das Spiel mit /"
"start\n"
"3. Gib <code>@mau_mau_bot</code> in deine Chatbox ein und drücke die "
"<b>Leertaste</b>, oder tippe auf den <code>via @mau_mau_bot</code>-Text "
"neben oder über den Nachrichten. Du siehst deine Karten (einige in grau), "
"zusätzliche Optionen wie z. B. Ziehen, und ein <b>?</b> um den Infos über "
"das laufende Spiel anzuzeigen. Die <b>grauen Karten</b> kannst du gerade "
"<b>nicht spielen</b>. Tippe eine der Optionen oder Karten an, um diese "
"Aktion auszuführen bzw. die Karte zu spielen. \n"
"Spieler können dem Spiel jederzeit beitreten. Um das Spiel zu verlassen, "
"benutze /leave. Wenn ein Spieler länger als 90 Sekunden braucht, kannst du "
"ihn mit /skip überspringen.\n"
"\n"
"<b>Sprache</b> und andere Einstellungen: /settings\n"
"Weitere Kommandos (nur Spiel-Ersteller):\n"
"/close - Lobby schließen\n"
"/open - Lobby öffnen\n"
"/enable_translations - Übersetze relevante Texte in alle im Spiel gesprochenen"
" Sprachen\n"
"/disable_translations - Verwende Englisch für diese Texte\n"
"\n"
"<b>Experimentell</b>: Spiele in mehreren Gruppen gleichzeitig. Um die "
"Gruppe, in der du deine Karte spielen willst, auszuwählen, tippe auf den "
"<code>Aktuelles Spiel: ...</code>-Button.\n"
"Wenn dir dieser Bot gefällt, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">bewerte ihn</a>, tritt dem <a href=\"https://telegram.me/"
"unobotupdates\">News-Channel</a> bei und kaufe ein UNO Kartenspiel."
#: bot.py:88
msgid ""
"This bot is Free Software and licensed under the AGPL. The code is available "
"here: \n"
"https://github.com/jh0ker/mau_mau_bot"
msgstr ""
"Dieser Bot ist Freie Software und lizenziert unter der AGPL. Der Quellcode "
"ist hier verfügbar:\n"
"https://github.com/jh0ker/mau_mau_bot"
#: bot.py:133
msgid ""
"Created a new game! Join the game with /join and start the game with /start"
msgstr ""
"Neues Spiel erstellt! Tritt dem Spiel mit /join bei und starte es mit /start"
#: bot.py:152
msgid "The lobby is closed"
msgstr "Die Lobby ist geschlossen"
#: bot.py:156
msgid "No game is running at the moment. Create a new game with /new"
msgstr "Zur Zeit läuft kein Spiel. Erstelle ein neues mit /new"
#: bot.py:162
msgid "You already joined the game. Start the game with /start"
msgstr "Du bist dem Spiel bereits beigetreten. Starte es mit /start"
#: bot.py:167
msgid "Joined the game"
msgstr "Spiel beigetreten"
#: bot.py:179 bot.py:191
msgid "You are not playing in a game in this group."
msgstr "Du spielst in keinem Spiel in dieser Gruppe."
#: bot.py:197 bot.py:258 bot.py:595
msgid "Game ended!"
msgstr "Spiel beendet!"
#: bot.py:201
msgid "Okay. Next Player: {name}"
msgstr "Okay. Nächster Spieler: {name}"
#: bot.py:219
msgid "Game not found."
msgstr "Spiel nicht gefunden."
#: bot.py:223
msgid "Back to last group"
msgstr "Zurück zur letzten Gruppe"
#: bot.py:227
msgid "Please switch to the group you selected!"
msgstr "Bitte wechsele zu der Gruppe, die du gewählt hast!"
#: bot.py:233
#, python-format
msgid ""
"Selected group: {group}\n"
"<b>Make sure that you switch to the correct group!</b>"
msgstr ""
"Ausgewählte Gruppe: {group}\n"
"<b>Stell sicher, dass du in die richtige Gruppe wechselst!</b>"
#: bot.py:260
#, python-format
msgid "Removing {name} from the game"
msgstr "Entferne {name} aus dem Spiel"
#: bot.py:273
msgid "There is no game running in this chat. Create a new one with /new"
msgstr ""
"In dieser Gruppe gibt es kein laufendes Spiel. Erstelle ein neues mit /new"
#: bot.py:278
msgid "The game has already started"
msgstr "Das Spiel hat bereits begonnen"
#: bot.py:281
msgid "At least two players must /join the game before you can start it"
msgstr "Es müssen mindestens zwei Spieler dem Spiel beitreten, bevor du es "
"starten kannst"
#: bot.py:297
#, python-format, fuzzy
msgid "First player: {name}\n"
"Use /close to stop people from joining the game.\n"
"Enable multi-translations with /enable_translations"
msgstr ""
"Erster Spieler: {name}\n"
"Benutze /close, um zu verhindern, dass weitere Spieler beitreten."
#: bot.py:321
msgid "Please select the group you want to play in."
msgstr "Bitte wähle die Gruppe, in der du spielen willst."
#: bot.py:335 bot.py:361
msgid "There is no running game in this chat."
msgstr "In dieser Gruppe läuft gerade kein Spiel."
#: bot.py:342
msgid "Closed the lobby. No more players can join this game."
msgstr ""
"Lobby geschlossen. Diesem Spiel können keine weiteren Spieler beitreten."
#: bot.py:348 bot.py:373
#, python-format
msgid "Only the game creator ({name}) can do that."
msgstr "Dies kann nur der Ersteller des Spiels ({name}) tun."
#: bot.py:349
#, python-format
msgid "Enabled multi-translations. Disable with /disable_translations"
msgstr "Multi-Übersetzungen aktiviert. Deaktivieren mit /disable_translations"
#: bot.py:377
#, python-format
msgid "Disabled multi-translations. Enable them again with /enable_translations"
msgstr "Multi-Übersetzungen deaktiviert. Aktiviere sie wieder mit "
"/enable_translations"
#: bot.py:368
msgid "Opened the lobby. New players may /join the game."
msgstr "Lobby geöffnet. Neue Spieler können nun beitreten."
#: bot.py:386
msgid "You are not playing in a game in this chat."
msgstr "Du spielst kein Spiel in dieser Gruppe."
#: bot.py:400
#, python-format
msgid "Please wait {time} seconds"
msgstr "Bitte warte {time} Sekunden"
#: bot.py:413
#, python-format
msgid ""
"Waiting time to skip this player has been reduced to {time} seconds.\n"
"Next player: {name}"
msgstr ""
"Die Wartezeit um diesen Spieler zu überspringen wurde auf {time} Sekunden "
"reduziert.\n"
"Nächster Spieler: {name}"
#: bot.py:424
#, python-format
msgid ""
"{name1} was skipped four times in a row and has been removed from the game.\n"
"Next player: {name2}"
msgstr ""
"{name1} wurde vier Mal hintereinander übersprungen und daher aus dem Spiel "
"entfernt.\n"
"Nächster Spieler: {name2}"
#: bot.py:432
#, python-format
msgid ""
"{name} was skipped four times in a row and has been removed from the game.\n"
"The game ended."
msgstr ""
"{name1} wurde vier Mal hintereinander übersprungen und daher aus dem Spiel "
"entfernt.\n"
"Das Spiel wurde beendet."
#: bot.py:455
msgid "All news here: https://telegram.me/unobotupdates"
msgstr "Alle News hier: https://telegram.me/unobotupdates"
#: bot.py:513
#, python-format
msgid "Current game: %s"
msgstr "Aktuelles Spiel: {game}"
#: bot.py:545
#, python-format
msgid "Cheat attempt by %s"
msgstr "{name} hat versucht zu schummeln!"
#: bot.py:562
msgid "Next player: {name}"
msgstr "Nächster Spieler: {name}"
#: bot.py:572
#, python-format
msgid "Waiting time for {name} has been reset to 90 seconds"
msgstr "Die Wartezeit für {name} wurde auf 90 Sekunden zurückgesetzt."
#: bot.py:585
msgid "Please choose a color"
msgstr "Bitte wähle eine Farbe"
#: bot.py:591
#, python-format
msgid "{name} won!"
msgstr "{name} hat gewonnen!"
#: bot.py:613 bot.py:635 bot.py:647
msgid "There are no more cards in the deck."
msgstr "Es sind keine Karten mehr im Deck."
#: bot.py:627
#, python-format
msgid "Bluff called! Giving 4 cards to {name}"
msgstr "Bluff gecalled! {name} bekommt 4 Karten."
#: bot.py:639
#, python-format
msgid "{name1} didn't bluff! Giving 6 cards to {name2}"
msgstr "{name1} hat nicht geblufft! {name2} bekommt 6 Karten."
#: results.py:38
msgid "Choose Color"
msgstr "Wähle Farbe"
#: results.py:56
msgid "Cards (tap for game state):"
msgstr "Karten (tippe für Spielinfo):"
#: results.py:60 results.py:123 results.py:165
msgid "Current player: {name}"
msgstr "Aktueller Spieler: {name}"
#: results.py:61 results.py:124 results.py:167
msgid "Last card: {card}"
msgstr "Letzte Karte: {card}"
#: results.py:62 results.py:125 results.py:168
msgid "Players: {player_list}"
msgstr "Spieler: {player_list}"
#: results.py:72
#, python-format
msgid "{name} ({number} cards)"
msgstr "{name} ({number} Karten)"
#: results.py:81
msgid "You are not playing"
msgstr "Du spielst gerade nicht"
#: results.py:83
msgid ""
"Not playing right now. Use /new to start a game or /join to join the current "
"game in this group"
msgstr ""
"Du spielst gerade nicht. Benutze /new um ein neues Spiel zu starten oder /"
"join, um einem bestehenden Spiel beizutreten."
#: results.py:95
msgid "The game wasn't started yet"
msgstr "Das Spiel wurde noch nicht gestartet."
#: results.py:97
msgid "Start the game with /start"
msgstr "Starte das Spiel mit /start"
#: results.py:108
#, python-format
msgid "Drawing 1 card"
msgstr "Zieht 1 Karte"
msgid "Drawing {number} cards"
msgstr "Zieht {number} Karten"
#: results.py:136
msgid "Pass"
msgstr "Passe"
#: results.py:148
msgid "I'm calling your bluff!"
msgstr "Ich glaube du bluffst!"
#: settings.py:39
msgid "Please edit your settings in a private chat with the bot."
msgstr "Bitte ändere deine Einstellungen in einem privaten Chat mit dem Bot."
#: settings.py:49
msgid "Enable statistics"
msgstr "Statistiken aktivieren"
#: settings.py:51
msgid "Delete all statistics"
msgstr "Alle Statistiken löschen"
#: settings.py:53
msgid "Language"
msgstr "Sprache"
#: settings.py:54
msgid "Settings"
msgstr "Einstellungen"
#: settings.py:68
msgid "Enabled statistics!"
msgstr "Statistiken aktiviert!"
#: settings.py:70
msgid "Select locale"
msgstr "Bitte Sprache auswählen"
#: settings.py:81
msgid "Deleted and disabled statistics!"
msgstr "Alle Statistiken gelöscht und deaktiviert!"
#: settings.py:94
msgid "Set locale!"
msgstr "Sprache gesetzt!"
#: simple_commands.py
msgid "You did not enable statistics. Use /settings in "
"a private chat with the bot to enable them."
msgstr "Du hast die Spiel-Statistiken nicht aktiviert. Aktiviere sie, mit dem "
"/settings-Kommando in einem privaten Chat mit dem Bot."
#: simple_commands.py
msgid "{number} games played"
msgstr "{number} gespielte Spiele"
#: simple_commands.py
msgid "{number} first places"
msgstr "{number}x 1. Platz"
#: simple_commands.py
msgid "{number} cards played"
msgstr "{number} gespielte Karten"
#: utils.py
msgid "{emoji} Green"
msgstr "{emoji} Grün"
#: utils.py
msgid "{emoji} Red"
msgstr "{emoji} Rot"
#: utils.py
msgid "{emoji} Blue"
msgstr "{emoji} Blau"
#: utils.py
msgid "{emoji} Yellow"
msgstr "{emoji} Gelb"

View File

@ -0,0 +1,447 @@
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
msgid ""
msgstr ""
"Project-Id-Version: uno_bot 0.1\n"
"Report-Msgid-Bugs-To: uno@jhoeke.de\n"
"POT-Creation-Date: 2016-05-22 20:02+0200\n"
"PO-Revision-Date: 2016-05-22 21:27+0200\n"
"Language-Team: en <uno@jhoeke.de>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: it_IT\n"
#: bot.py:60
msgid ""
"Follow these steps:\n"
"\n"
"1. Add this bot to a group\n"
"2. In the group, start a new game with /new or join an already running game "
"with /join\n"
"3. After at least two players have joined, start the game with /start\n"
"4. Type <code>@mau_mau_bot</code> into your chat box and hit <b>space</b>, "
"or click the <code>via @mau_mau_bot</code> text next to messages. You will "
"see your cards (some greyed out), any extra options like drawing, and a <b>?"
"</b> to see the current game state. The <b>greyed out cards</b> are those "
"you <b>can not play</b> at the moment. Tap an option to execute the selected "
"action.\n"
"Players can join the game at any time. To leave a game, use /leave. If a "
"player takes more than 90 seconds to play, you can use /skip to skip that "
"player.\n"
"\n"
"<b>Language</b> and other settings: /settings\n"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\n"
"/enable_translations - Translate relevant texts into all languages spoken in "
"a game\n"
"/disable_translations - Use English for those texts\n"
"\n"
"<b>Experimental:</b> Play in multiple groups at the same time. Press the "
"<code>Current game: ...</code> button and select the group you want to play "
"a card in.\n"
"If you enjoy this bot, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">rate me</a>, join the <a href=\"https://telegram.me/"
"unobotupdates\">update channel</a> and buy an UNO card game."
msgstr ""
"Segui questi passaggi:\n"
"\n"
"1. Aggiungi questo bot ad un gruppo\n"
"2. Nel gruppo, inizia un nuovo gioco con /new o unisciti ad una partita già "
"avviata con /join\n"
"3. Quando almeno due giocatori si sono uniti alla partita, si può iniziare "
"con /start\n"
"4. Digita <code>@mau_mau_bot</code> nella casella di testo e premi "
"<b>spazio</b>, o clicca sul testo <code>via @mau_mau_bot</code> vicino ai "
"messaggi. Potrai così vedere le tue carte (alcune in grigio), le opzioni "
"extra come pesca e <b>?</b> per vedere lo stato della partita attuale. Le "
"<b>carte in grigio</b> sono quelle che <b>non puoi giocare</b> al momento. "
"Premi un'opzione per eseguire l'azione selezionata.\n"
"I giocatori possono unirsi alla partita in qualsiasi momento. Per lasciare "
"la partita, usa /leave. Se un giocatore impiega più di 90 secondi a giocare, "
"puoi usare /skip per saltare il turno di quel giocatore.\n"
"\n"
"<b>Lingua</b> e altre opzioni: /settings\n"
"Altri comandi (utilizzabili solo da chi ha creato la partita):\n"
"/close - Chiude la lobby, nessun giocatore può unirsi al gioco dopo che "
"questo è già avviato.\n"
"/open - Apre la lobby, i giocatori possono unirsi al gioco in qualsiasi "
"momento. \n"
"/enable_translations - Traduce i messaggi importanti nelle lingue parlate "
"in gioco\n"
"/disable_translations - Usa l'inglese per questi messaggi\n"
"\n"
"<b>Sperimentale:</b> Gioca in più gruppi allo stesso tempo. Premi il tasto "
"<code>Partita corrente: ...</code> e scegli il gruppo dove vuoi giocare la "
"carta.\n"
"Se ti piace questo bot, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">votami</a>, segui gli <a href=\"https://telegram.me/"
"unobotupdates\">aggiornamenti</a> e compra il gioco di carte UNO. "
#: bot.py:88
msgid ""
"This bot is Free Software and licensed under the AGPL. The code is available "
"here: \n"
"https://github.com/jh0ker/mau_mau_bot"
msgstr ""
"Questo bot è Free Software e certificato con AGPL. Il codice è disponibile "
"qui:\n"
"https://github.com/jh0ker/mau_mau_bot"
#: bot.py:133
msgid ""
"Created a new game! Join the game with /join and start the game with /start"
msgstr ""
"È stato creato un nuovo gioco! Unisciti con /join e inizia la partita con /"
"start"
#: bot.py:152
msgid "The lobby is closed"
msgstr "La lobby è chiusa. Nessuno nuovo giocatore può unirsi alla partita."
#: bot.py:156
msgid "No game is running at the moment. Create a new game with /new"
msgstr ""
"Non c'è nessuna partita aperta al momento. Inizia un nuovo gioco con /new"
#: bot.py:162
msgid "You already joined the game. Start the game with /start"
msgstr "Ti sei già unito al gioco. Inizia la partita con /start"
#: bot.py:167
msgid "Joined the game"
msgstr "Si è unito al gioco"
#: bot.py:179 bot.py:191
msgid "You are not playing in a game in this group."
msgstr "Non stai giocando alcuna partita in questo gruppo."
#: bot.py:197 bot.py:258 bot.py:595
msgid "Game ended!"
msgstr "La partita è conclusa!"
#: bot.py:201
msgid "Okay. Next Player: {name}"
msgstr "Okay. Prossimo giocatore: {name}"
#: bot.py:219
msgid "Game not found."
msgstr "Partita non trovata."
#: bot.py:223
msgid "Back to last group"
msgstr "Vai all'ultimo gruppo. "
#: bot.py:227
msgid "Please switch to the group you selected!"
msgstr "Per favore, spostati sul gruppo che hai selezionato!"
#: bot.py:233
#, python-format
msgid ""
"Selected group: {group}\n"
"<b>Make sure that you switch to the correct group!</b>"
msgstr ""
"Gruppo selezionato: {group}\n"
"<b>Assicurati di spostarti nel gruppo giusto!</b>"
#: bot.py:260
#, python-format
msgid "Removing {name} from the game"
msgstr "Sto rimuovendo {name} dalla partita"
#: bot.py:273
msgid "There is no game running in this chat. Create a new one with /new"
msgstr ""
"Non c'è alcuna partita aperta in questa chat. Creane una nuova con /new"
#: bot.py:278
msgid "The game has already started"
msgstr "La partita è già iniziata"
#: bot.py:281
msgid "At least two players must /join the game before you can start it"
msgstr ""
"Ci devono essere almeno due giocatori che hanno cliccato /join per iniziare "
"la partita"
#: bot.py:297
#, python-format
msgid ""
"First player: {name}\n"
"Use /close to stop people from joining the game.\n"
"Enable multi-translations with /enable_translations"
msgstr ""
"Primo giocatore: {name}\n"
"Usa /close per impedire a nuovi giocatori di unirsi alla partita.\n"
"Abilita la funzione di multi-traduzione con /enable_translations"
#: bot.py:321
msgid "Please select the group you want to play in."
msgstr "Per favore, seleziona il gruppo dove vuoi giocare."
#: bot.py:335 bot.py:361
msgid "There is no running game in this chat."
msgstr "Non c'è alcuna partita aperta in questa chat."
#: bot.py:342
msgid "Closed the lobby. No more players can join this game."
msgstr ""
"La lobby è chiusa. Nessun nuovo giocatore può unirsi alla partita corrente."
#: bot.py:348 bot.py:373
#, python-format
msgid "Only the game creator ({name}) can do that."
msgstr "Solo il creatore del gioco ({name}) può farlo. "
#: bot.py:349
#, python-format
msgid "Enabled multi-translations. Disable with /disable_translations"
msgstr "Multi-traduzione abilitata. Disabilitala con /disable_translations"
#: bot.py:377
#, python-format
msgid ""
"Disabled multi-translations. Enable them again with /enable_translations"
msgstr "Multi-traduzione disabilitata. Abilitala con /enable_translations"
#: bot.py:368
msgid "Opened the lobby. New players may /join the game."
msgstr ""
"La lobby è aperta. Nuovi giocatori possono unirsi alla partita con /join"
#: bot.py:386
msgid "You are not playing in a game in this chat."
msgstr "Non stai giocando alcuna partita in questa chat. "
#: bot.py:400
#, python-format
msgid "Please wait {time} seconds"
msgstr "Per favore, attendi {time} secondi"
#: bot.py:413
#, python-format
msgid ""
"Waiting time to skip this player has been reduced to {time} seconds.\n"
"Next player: {name}"
msgstr ""
"Il tempo di attesa per saltare questo giocatore è stata ridotta a {time} "
"secondi.\n"
"Prossimo giocatore: {name}"
#: bot.py:424
#, python-format
msgid ""
"{name1} was skipped four times in a row and has been removed from the game.\n"
"Next player: {name2}"
msgstr ""
"{name1} è stato saltato per quattro turni ed è stato rimosso dalla partita.\n"
" \n"
"Prossimo giocatore: {name2}"
#: bot.py:432
#, python-format
msgid ""
"{name} was skipped four times in a row and has been removed from the game.\n"
"The game ended."
msgstr ""
"{name} è stato saltato per quattro turni ed è stato rimosso dalla partita.\n"
" \n"
"La partita è conclusa."
#: bot.py:455
msgid "All news here: https://telegram.me/unobotupdates"
msgstr "Tutte le novità le trovi qui: https://telegram.me/unobotupdates"
#: bot.py:513
#, python-format
msgid "Current game: {group}"
msgstr "Partita corrente: {group}"
#: bot.py:545
#, python-format
msgid "Cheat attempt by {name}"
msgstr "{name} ha tentato di barare! "
#: bot.py:562
msgid "Next player: {name}"
msgstr "Prossimo giocatore: {name}"
#: bot.py:572
#, python-format
msgid "Waiting time for {name} has been reset to 90 seconds"
msgstr "Il tempo di attesa per {name} è stato reimpostato a 90 secondi"
#: bot.py:585
msgid "Please choose a color"
msgstr "Per favore, scegli un colore"
#: bot.py:591
#, python-format
msgid "{name} won!"
msgstr "{name} ha vinto!"
#: bot.py:613 bot.py:635 bot.py:647
msgid "There are no more cards in the deck."
msgstr "Non ci sono più carte sul tavolo."
#: bot.py:627
#, python-format
msgid "Bluff called! Giving 4 cards to {name}"
msgstr "Il baro è stato scoperto! Sto dando 4 carte a {name}"
#: bot.py:639
#, python-format
msgid "{name1} didn't bluff! Giving 6 cards to {name2}"
msgstr "{name1} non ha barato! Sto dando 6 carte a {name2}"
#: results.py:38
msgid "Choose Color"
msgstr "Scegli il colore"
#: results.py:56
msgid "Cards (tap for game state):"
msgstr "Carte (clicca per vedere lo stato della partita):"
#: results.py:60 results.py:123 results.py:165
msgid "Current player: {name}"
msgstr "Giocatore attuale: {name}"
#: results.py:61 results.py:124 results.py:167
msgid "Last card: {card}"
msgstr "Ultima carta: {card}"
#: results.py:62 results.py:125 results.py:168
msgid "Players: {player_list}"
msgstr "Giocatori: {player_list}"
#: results.py:72
#, python-format
msgid "{name} ({number} cards)"
msgstr "{name} ({number} carte)"
#: results.py:81
msgid "You are not playing"
msgstr "Non stai giocando in questa partita. "
#: results.py:83
msgid ""
"Not playing right now. Use /new to start a game or /join to join the current "
"game in this group"
msgstr ""
"Non stai giocando in questo momento. Usa /new per iniziare un nuovo gioco o /"
"join per partecipare alla partita avviata in questo gruppo"
#: results.py:95
msgid "The game wasn't started yet"
msgstr "Il gioco non è ancora iniziato"
#: results.py:97
msgid "Start the game with /start"
msgstr "Inizia il gioco con /start"
#: results.py:108
#, python-format
msgid "Drawing 1 card"
msgstr "Pesca 1 carta"
msgid "Drawing {number} cards"
msgstr "Pesca {number} carte"
#: results.py:136
msgid "Pass"
msgstr "Passa"
#: results.py:148
msgid "I'm calling your bluff!"
msgstr "Penso che tu stia barando!"
#: settings.py:39
msgid "Please edit your settings in a private chat with the bot."
msgstr ""
"Per favore, modifica le tue impostazioni in una chat privata con il bot."
#: settings.py:49
msgid "Enable statistics"
msgstr "Abilita le statistiche"
#: settings.py:51
msgid "Delete all statistics"
msgstr "Elimina tutte le statistiche"
#: settings.py:53
msgid "Language"
msgstr "Lingua"
#: settings.py:54
msgid "Settings"
msgstr "Impostazioni"
#: settings.py:68
msgid "Enabled statistics!"
msgstr "Statistiche abilitate!"
#: settings.py:70
msgid "Select locale"
msgstr "Seleziona locale"
#: settings.py:81
msgid "Deleted and disabled statistics!"
msgstr "Le statistiche sono state eliminate e disabilitate!"
#: settings.py:94
msgid "Set locale!"
msgstr "Imposta locale!"
#: simple_commands.py
msgid ""
"You did not enable statistics. Use /settings in a private chat with the bot "
"to enable them."
msgstr ""
"Non hai abilitato le statistiche. Usa /settings in una chat privata col bot "
"per abilitarle."
#: simple_commands.py
msgid "{number} games played"
msgstr "{number} partite giocate"
#: simple_commands.py
msgid "{number} first places"
msgstr "{number} primi posti"
#: simple_commands.py
msgid "{number} cards played"
msgstr "{number} carte giocate"
#: utils.py
msgid "{emoji} Green"
msgstr "{emoji} Verde"
#: utils.py
msgid "{emoji} Red"
msgstr "{emoji} Rosso"
#: utils.py
msgid "{emoji} Blue"
msgstr "{emoji} Blu"
#: utils.py
msgid "{emoji} Yellow"
msgstr "{emoji} Giallo"

382
locales/unobot.pot Normal file
View File

@ -0,0 +1,382 @@
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#: bot.py:224
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: uno_bot 0.1\n"
"Report-Msgid-Bugs-To: uno@jhoeke.de\n"
"POT-Creation-Date: 2016-05-19 22:38+0200\n"
"PO-Revision-Date: 2016-05-19 22:38+0200\n"
"Last-Translator: Jannes Höke <uno@jhoeke.de>\n"
"Language-Team: en <uno@jhoeke.de>\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: utf-8\n"
#: bot.py:60
#, fuzzy
msgid "Follow these steps:\n"
"\n"
"1. Add this bot to a group\n"
"2. In the group, start a new game with /new or join an already running game "
"with /join\n"
"3. After at least two players have joined, start the game with /start\n"
"4. Type <code>@mau_mau_bot</code> into your chat box and hit <b>space</b>, "
"or click the <code>via @mau_mau_bot</code> text next to messages. You will "
"see your cards (some greyed out), any extra options like drawing, and a <b>?"
"</b> to see the current game state. The <b>greyed out cards</b> are those "
"you <b>can not play</b> at the moment. Tap an option to execute the selected "
"action.\n"
"Players can join the game at any time. To leave a game, use /leave. If a "
"player takes more than 90 seconds to play, you can use /skip to skip that "
"player.\n"
"\n"
"<b>Language</b> and other settings: /settings\n"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\n"
"/enable_translations - Translate relevant texts into all "
"languages spoken in a game\n"
"/disable_translations - Use English for those texts\n\n"
"<b>Experimental:</b> Play in multiple groups at the same time. Press the "
"<code>Current game: ...</code> button and select the group you want to play "
"a card in.\n"
"If you enjoy this bot, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">rate me</a>, join the <a href=\"https://telegram.me/"
"unobotupdates\">update channel</a> and buy an UNO card game."
msgstr ""
#: bot.py:88
msgid "This bot is Free Software and licensed under the AGPL. The code is available "
"here: \n"
"https://github.com/jh0ker/mau_mau_bot"
msgstr ""
#: bot.py:133
msgid "Created a new game! Join the game with /join and start the game with /start"
msgstr ""
#: bot.py:152
msgid "The lobby is closed"
msgstr ""
#: bot.py:156
msgid "No game is running at the moment. Create a new game with /new"
msgstr ""
#: bot.py:162
msgid "You already joined the game. Start the game with /start"
msgstr ""
#: bot.py:167
msgid "Joined the game"
msgstr ""
#: bot.py:179 bot.py:191
msgid "You are not playing in a game in this group."
msgstr ""
#: bot.py:197 bot.py:258 bot.py:595
msgid "Game ended!"
msgstr ""
#: bot.py:201
msgid "Okay. Next Player: {name}"
msgstr ""
#: bot.py:219
msgid "Game not found."
msgstr ""
#: bot.py:223
msgid "Back to last group"
msgstr ""
#: bot.py:227
msgid "Please switch to the group you selected!"
msgstr ""
#: bot.py:233
#, python-format
msgid "Selected group: {group}\n"
"<b>Make sure that you switch to the correct group!</b>"
msgstr ""
#: bot.py:260
#, python-format
msgid "Removing {name} from the game"
msgstr ""
#: bot.py:273
msgid "There is no game running in this chat. Create a new one with /new"
msgstr ""
#: bot.py:278
msgid "The game has already started"
msgstr ""
#: bot.py:281
msgid "At least two players must /join the game before you can start it"
msgstr ""
#: bot.py:297
#, python-format, fuzzy
msgid "First player: {name}\n"
"Use /close to stop people from joining the game.\n"
"Enable multi-translations with /enable_translations"
msgstr ""
#: bot.py:321
msgid "Please select the group you want to play in."
msgstr ""
#: bot.py:335 bot.py:361
msgid "There is no running game in this chat."
msgstr ""
#: bot.py:342
msgid "Closed the lobby. No more players can join this game."
msgstr ""
#: bot.py:348 bot.py:373
#, python-format
msgid "Only the game creator ({name}) can do that."
msgstr ""
#: bot.py:349
#, python-format
msgid "Enabled multi-translations. Disable with /disable_translations"
msgstr ""
#: bot.py:377
#, python-format
msgid "Disabled multi-translations. Enable them again with /enable_translations"
msgstr ""
#: bot.py:368
msgid "Opened the lobby. New players may /join the game."
msgstr ""
#: bot.py:386
msgid "You are not playing in a game in this chat."
msgstr ""
#: bot.py:400
#, python-format
msgid "Please wait {time} seconds"
msgstr ""
#: bot.py:413
#, python-format
msgid "Waiting time to skip this player has been reduced to {time} seconds.\n"
"Next player: {name}"
msgstr ""
#: bot.py:424
#, python-format
msgid "{name1} was skipped four times in a row and has been removed from the game.\n"
"Next player: {name2}"
msgstr ""
#: bot.py:432
#, python-format
msgid "{name} was skipped four times in a row and has been removed from the game.\n"
"The game ended."
msgstr ""
#: bot.py:455
msgid "All news here: https://telegram.me/unobotupdates"
msgstr ""
#: bot.py:513
#, python-format
msgid "Current game: {group}"
msgstr ""
#: bot.py:545
#, python-format
msgid "Cheat attempt by {name}"
msgstr ""
#: bot.py:562
msgid "Next player: {name}"
msgstr ""
#: bot.py:572
#, python-format
msgid "Waiting time for {name} has been reset to 90 seconds"
msgstr ""
#: bot.py:585
msgid "Please choose a color"
msgstr ""
#: bot.py:591
#, python-format
msgid "{name} won!"
msgstr ""
#: bot.py:613 bot.py:635 bot.py:647
msgid "There are no more cards in the deck."
msgstr ""
#: bot.py:627
#, python-format
msgid "Bluff called! Giving 4 cards to {name}"
msgstr ""
#: bot.py:639
#, python-format
msgid "{name1} didn't bluff! Giving 6 cards to {name2}"
msgstr ""
#: results.py:38
msgid "Choose Color"
msgstr ""
#: results.py:56
msgid "Cards (tap for game state):"
msgstr ""
#: results.py:60 results.py:123 results.py:165
msgid "Current player: {name}"
msgstr ""
#: results.py:61 results.py:124 results.py:167
msgid "Last card: {card}"
msgstr ""
#: results.py:62 results.py:125 results.py:168
msgid "Players: {player_list}"
msgstr ""
#: results.py:72
#, python-format
msgid "{name} ({number} cards)"
msgstr ""
#: results.py:81
msgid "You are not playing"
msgstr ""
#: results.py:83
msgid "Not playing right now. Use /new to start a game or /join to join the current "
"game in this group"
msgstr ""
#: results.py:95
msgid "The game wasn't started yet"
msgstr ""
#: results.py:97
msgid "Start the game with /start"
msgstr ""
#: results.py:108
#, python-format
msgid "Drawing 1 card"
msgstr ""
msgid "Drawing {number} cards"
msgstr ""
#: results.py:136
msgid "Pass"
msgstr ""
#: results.py:148
msgid "I'm calling your bluff!"
msgstr ""
#: settings.py:39
msgid "Please edit your settings in a private chat with the bot."
msgstr ""
#: settings.py:49
msgid "Enable statistics"
msgstr ""
#: settings.py:51
msgid "Delete all statistics"
msgstr ""
#: settings.py:53
msgid "Language"
msgstr ""
#: settings.py:54
msgid "Settings"
msgstr ""
#: settings.py:68
msgid "Enabled statistics!"
msgstr ""
#: settings.py:70
msgid "Select locale"
msgstr ""
#: settings.py:81
msgid "Deleted and disabled statistics!"
msgstr ""
#: settings.py:94
msgid "Set locale!"
msgstr ""
#: simple_commands.py
msgid "You did not enable statistics. Use /settings in "
"a private chat with the bot to enable them."
msgstr ""
#: simple_commands.py
msgid "{number} games played"
msgstr ""
#: simple_commands.py
msgid "{number} first places"
msgstr ""
#: simple_commands.py
msgid "{number} cards played"
msgstr ""
#: utils.py
msgid "{emoji} Green"
msgstr ""
#: utils.py
msgid "{emoji} Red"
msgstr ""
#: utils.py
msgid "{emoji} Blue"
msgstr ""
#: utils.py
msgid "{emoji} Yellow"
msgstr ""

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -20,9 +21,8 @@
import logging import logging
from datetime import datetime from datetime import datetime
from telegram import User
import card as c import card as c
from errors import DeckEmptyError
class Player(object): class Player(object):
@ -33,12 +33,21 @@ class Player(object):
other players by placing itself behind the current player. other players by placing itself behind the current player.
""" """
def __init__(self, game, user, ai=False): def __init__(self, game, user):
self.cards = list() self.cards = list()
self.game = game self.game = game
self.user = user self.user = user
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
try:
for i in range(7):
self.cards.append(self.game.deck.draw())
except DeckEmptyError:
for card in self.cards:
self.game.deck.dismiss(card)
raise
# Check if this player is the first player in this game. # Check if this player is the first player in this game.
if game.current_player: if game.current_player:
self.next = game.current_player self.next = game.current_player
@ -50,21 +59,14 @@ class Player(object):
self._prev = self self._prev = self
game.current_player = self game.current_player = self
for i in range(7):
self.cards.append(self.game.deck.draw())
self.bluffing = False self.bluffing = False
self.drew = False self.drew = False
self.anti_cheat = 0 self.anti_cheat = 0
self.turn_started = datetime.now() self.turn_started = datetime.now()
self.waiting_time = 90 self.waiting_time = 90
self.ai = ai
if ai and not user:
self.user = User(-1, "Computer")
def leave(self): def leave(self):
""" Leave the current game """ """Removes player from the game and closes the gap in the list"""
if self.next is self: if self.next is self:
return return
@ -79,10 +81,10 @@ class Player(object):
self.cards = list() self.cards = list()
def __repr__(self): def __repr__(self):
return repr(self.user) if not self.ai else "computer" return repr(self.user)
def __str__(self): def __str__(self):
return str(self.user) if not self.ai else "Computer" return str(self.user)
@property @property
def next(self): def next(self):
@ -106,8 +108,28 @@ class Player(object):
else: else:
self._next = player self._next = player
def draw(self):
"""Draws 1+ cards from the deck, depending on the draw counter"""
_amount = self.game.draw_counter or 1
try:
for i in range(_amount):
self.cards.append(self.game.deck.draw())
except DeckEmptyError:
raise
finally:
self.game.draw_counter = 0
self.drew = True
def play(self, card):
"""Plays a card and removes it from hand"""
self.cards.remove(card)
self.game.play_card(card)
def playable_cards(self): def playable_cards(self):
""" Returns a list of the cards this player can play right now """ """Returns a list of the cards this player can play right now"""
playable = list() playable = list()
last = self.game.last_card last = self.game.last_card
@ -121,7 +143,7 @@ class Player(object):
# You may only play a +4 if you have no cards of the correct color # You may only play a +4 if you have no cards of the correct color
self.bluffing = False self.bluffing = False
for card in cards: for card in cards:
if self.card_playable(card, playable): if self._card_playable(card):
self.logger.debug("Matching!") self.logger.debug("Matching!")
playable.append(card) playable.append(card)
@ -133,8 +155,8 @@ class Player(object):
return playable return playable
def card_playable(self, card, playable): def _card_playable(self, card):
""" Check a single card if it can be played """ """Check a single card if it can be played"""
is_playable = True is_playable = True
last = self.game.last_card last = self.game.last_card
@ -155,9 +177,8 @@ class Player(object):
(card.special == c.CHOOSE or card.special == c.DRAW_FOUR): (card.special == c.CHOOSE or card.special == c.DRAW_FOUR):
self.logger.debug("Can't play colorchooser on another one") self.logger.debug("Can't play colorchooser on another one")
is_playable = False is_playable = False
elif not last.color or card in playable: elif not last.color:
self.logger.debug("Last card has no color or the card was " self.logger.debug("Last card has no color")
"already added to the list")
is_playable = False is_playable = False
return is_playable return is_playable

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -17,129 +18,136 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Defines helper functions to build the inline result list"""
from uuid import uuid4 from uuid import uuid4
from telegram import InlineQueryResultArticle, InputTextMessageContent, \ from telegram import InlineQueryResultArticle, InputTextMessageContent, \
InlineQueryResultCachedSticker as Sticker InlineQueryResultCachedSticker as Sticker
import card as c import card as c
from utils import * from utils import display_color, display_color_group, display_name, \
list_subtract, _, __
def add_choose_color(results): def add_choose_color(results, game):
"""Add choose color options"""
for color in c.COLORS: for color in c.COLORS:
results.append( results.append(
InlineQueryResultArticle( InlineQueryResultArticle(
id=color, id=color,
title="Choose Color", title=_("Choose Color"),
description=display_color(color), description=display_color(color),
input_message_content= input_message_content=
InputTextMessageContent(display_color(color)) InputTextMessageContent(display_color_group(color, game))
) )
) )
def add_other_cards(playable, player, results, game): def add_other_cards(playable, player, results, game):
"""Add hand cards when choosing colors"""
if not playable: if not playable:
playable = list() playable = list()
players = player_list(game)
results.append( results.append(
InlineQueryResultArticle( InlineQueryResultArticle(
"hand", "hand",
title="Cards (tap for game state):", title=_("Cards (tap for game state):"),
description=', '.join([repr(card) for card in description=', '.join([repr(card) for card in
list_subtract(player.cards, playable)]), list_subtract(player.cards, playable)]),
input_message_content=InputTextMessageContent( input_message_content=game_info(game)
"Current player: " + display_name(game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players))
) )
) )
def player_list(game): def player_list(game):
players = list() """Generate list of player strings"""
for player in game.players: return [_("{name} ({number} cards)")
add_player(player, players) .format(name=player.user.first_name, number=len(player.cards))
return players for player in game.players]
def add_no_game(results): def add_no_game(results):
"""Add text result if user is not playing"""
results.append( results.append(
InlineQueryResultArticle( InlineQueryResultArticle(
"nogame", "nogame",
title="You are not playing", title=_("You are not playing"),
input_message_content= input_message_content=
InputTextMessageContent('Not playing right now. Use /new to start ' InputTextMessageContent(_('Not playing right now. Use /new to '
'a game or /join to join the current game ' 'start a game or /join to join the '
'in this group') 'current game in this group'))
) )
) )
def add_not_started(results): def add_not_started(results):
"""Add text result if the game has not yet started"""
results.append( results.append(
InlineQueryResultArticle( InlineQueryResultArticle(
"nogame", "nogame",
title="The game wasn't started yet", title=_("The game wasn't started yet"),
input_message_content= input_message_content=
InputTextMessageContent('Start the game with /start') InputTextMessageContent(_('Start the game with /start'))
) )
) )
def add_draw(player, results): def add_draw(player, results):
"""Add option to draw"""
n = player.game.draw_counter or 1
results.append( results.append(
Sticker( Sticker(
"draw", sticker_file_id=c.STICKERS['option_draw'], "draw", sticker_file_id=c.STICKERS['option_draw'],
input_message_content= input_message_content=
InputTextMessageContent('Drawing %d card(s)' InputTextMessageContent(__('Drawing 1 card', player.game.translate)
% (player.game.draw_counter or 1)) if n == 1 else
__('Drawing {number} cards',
player.game.translate)
.format(number=n))
) )
) )
def add_gameinfo(game, results): def add_gameinfo(game, results):
players = player_list(game) """Add option to show game info"""
results.append( results.append(
Sticker( Sticker(
"gameinfo", "gameinfo",
sticker_file_id=c.STICKERS['option_info'], sticker_file_id=c.STICKERS['option_info'],
input_message_content=InputTextMessageContent( input_message_content=game_info(game)
"Current player: " + display_name(game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players))
) )
) )
def add_pass(results): def add_pass(results, game):
"""Add option to pass"""
results.append( results.append(
Sticker( Sticker(
"pass", sticker_file_id=c.STICKERS['option_pass'], "pass", sticker_file_id=c.STICKERS['option_pass'],
input_message_content=InputTextMessageContent('Pass') input_message_content=InputTextMessageContent(__('Pass',
game.translate))
) )
) )
def add_call_bluff(results): def add_call_bluff(results, game):
"""Add option to call a bluff"""
results.append( results.append(
Sticker( Sticker(
"call_bluff", "call_bluff",
sticker_file_id=c.STICKERS['option_bluff'], sticker_file_id=c.STICKERS['option_bluff'],
input_message_content= input_message_content=
InputTextMessageContent("I'm calling your bluff!") InputTextMessageContent(__("I'm calling your bluff!",
game.translate))
) )
) )
def add_play_card(game, card, results, can_play): def add_card(game, card, results, can_play):
players = player_list(game) """Add an option that represents a card"""
if can_play: if can_play:
results.append( results.append(
@ -148,16 +156,18 @@ def add_play_card(game, card, results, can_play):
else: else:
results.append( results.append(
Sticker(str(uuid4()), sticker_file_id=c.STICKERS_GREY[str(card)], Sticker(str(uuid4()), sticker_file_id=c.STICKERS_GREY[str(card)],
input_message_content=InputTextMessageContent( input_message_content=game_info(game))
"Current player: " + display_name(
game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players)))
) )
def add_player(itplayer, players): def game_info(game):
players.append(itplayer.user.first_name + " (%d cards)" players = player_list(game)
% len(itplayer.cards)) return InputTextMessageContent(
_("Current player: {name}")
.format(name=display_name(game.current_player.user)) +
"\n" +
_("Last card: {card}").format(card=repr(game.last_card)) +
"\n" +
_("Players: {player_list}")
.format(player_list=" -> ".join(players))
)

106
settings.py Normal file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from telegram import ReplyKeyboardMarkup, Emoji
from telegram.ext import CommandHandler, RegexHandler
from utils import send_async
from user_setting import UserSetting
from utils import _, user_locale
from shared_vars import dispatcher
available_locales = [['en_US', 'de_DE'],
['it_IT']]
@user_locale
def show_settings(bot, update):
chat = update.message.chat
if update.message.chat.type != 'private':
send_async(bot, chat.id,
text=_("Please edit your settings in a private chat with "
"the bot."))
return
us = UserSetting.get(id=update.message.from_user.id)
if not us:
us = UserSetting(id=update.message.from_user.id)
if not us.stats:
stats = Emoji.BAR_CHART + ' ' + _("Enable statistics")
else:
stats = Emoji.CROSS_MARK + ' ' + _("Delete all statistics")
kb = [[stats], [Emoji.EARTH_GLOBE_EUROPE_AFRICA + ' ' + _("Language")]]
send_async(bot, chat.id, text=Emoji.WRENCH + ' ' + _("Settings"),
reply_markup=ReplyKeyboardMarkup(keyboard=kb,
one_time_keyboard=True))
@user_locale
def kb_select(bot, update, groups):
chat = update.message.chat
user = update.message.from_user
option = groups[0]
if option == Emoji.BAR_CHART:
us = UserSetting.get(id=user.id)
us.stats = True
send_async(bot, chat.id, text=_("Enabled statistics!"))
elif option == Emoji.EARTH_GLOBE_EUROPE_AFRICA:
send_async(bot, chat.id, text=_("Select locale"),
reply_markup=ReplyKeyboardMarkup(keyboard=available_locales,
one_time_keyboard=True))
elif option == Emoji.CROSS_MARK:
us = UserSetting.get(id=user.id)
us.stats = False
us.first_places = 0
us.games_played = 0
us.cards_played = 0
send_async(bot, chat.id, text=_("Deleted and disabled statistics!"))
@user_locale
def locale_select(bot, update, groups):
chat = update.message.chat
user = update.message.from_user
option = groups[0]
if option in [locale for row in available_locales for locale in row]:
us = UserSetting.get(id=user.id)
us.lang = option
_.push(option)
send_async(bot, chat.id, text=_("Set locale!"))
_.pop()
dispatcher.add_handler(CommandHandler('settings', show_settings))
dispatcher.add_handler(RegexHandler('^([' + Emoji.BAR_CHART +
Emoji.EARTH_GLOBE_EUROPE_AFRICA +
Emoji.CROSS_MARK + ']) .+$',
kb_select, pass_groups=True))
dispatcher.add_handler(RegexHandler(r'^(\w\w_\w\w)$',
locale_select, pass_groups=True))

37
shared_vars.py Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram.ext import Updater
from telegram.utils.botan import Botan
from game_manager import GameManager
from database import db
from credentials import TOKEN, BOTAN_TOKEN
db.bind('sqlite', 'uno.sqlite3', create_db=True)
db.generate_mapping(create_tables=True)
gm = GameManager()
updater = Updater(token=TOKEN, workers=32)
dispatcher = updater.dispatcher
botan = False
if BOTAN_TOKEN:
botan = Botan(BOTAN_TOKEN)

110
simple_commands.py Normal file
View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram import ParseMode
from telegram.ext import CommandHandler
from user_setting import UserSetting
from utils import _, send_async, user_locale
from shared_vars import dispatcher
help_text = ("Follow these steps:\n\n"
"1. Add this bot to a group\n"
"2. In the group, start a new game with /new or join an already"
" running game with /join\n"
"3. After at least two players have joined, start the game with"
" /start\n"
"4. Type <code>@mau_mau_bot</code> into your chat box and hit "
"<b>space</b>, or click the <code>via @mau_mau_bot</code> text "
"next to messages. You will see your cards (some greyed out), "
"any extra options like drawing, and a <b>?</b> to see the "
"current game state. The <b>greyed out cards</b> are those you "
"<b>can not play</b> at the moment. Tap an option to execute "
"the selected action.\n"
"Players can join the game at any time. To leave a game, "
"use /leave. If a player takes more than 90 seconds to play, "
"you can use /skip to skip that player.\n\n"
"<b>Language</b> and other settings: /settings\n"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\n"
"/enable_translations - Translate relevant texts into all "
"languages spoken in a game\n"
"/disable_translations - Use English for those texts\n\n"
"<b>Experimental:</b> Play in multiple groups at the same time. "
"Press the <code>Current game: ...</code> button and select the "
"group you want to play a card in.\n"
"If you enjoy this bot, "
"<a href=\"https://telegram.me/storebot?start=mau_mau_bot\">"
"rate me</a>, join the "
"<a href=\"https://telegram.me/unobotupdates\">update channel</a>"
" and buy an UNO card game.")
source_text = ("This bot is Free Software and licensed under the AGPL. "
"The code is available here: \n"
"https://github.com/jh0ker/mau_mau_bot")
@user_locale
def help(bot, update):
"""Handler for the /help command"""
send_async(bot, update.message.chat_id, text=_(help_text),
parse_mode=ParseMode.HTML, disable_web_page_preview=True)
@user_locale
def source(bot, update):
"""Handler for the /help command"""
send_async(bot, update.message.chat_id, text=_(source_text),
parse_mode=ParseMode.HTML, disable_web_page_preview=True)
@user_locale
def news(bot, update):
"""Handler for the /news command"""
send_async(bot, update.message.chat_id,
text=_("All news here: https://telegram.me/unobotupdates"),
disable_web_page_preview=True)
@user_locale
def stats(bot, update):
user = update.message.from_user
us = UserSetting.get(id=user.id)
if not us or not us.stats:
send_async(bot, update.message.chat_id,
text=_("You did not enable statistics. Use /settings in "
"a private chat with the bot to enable them."))
else:
stats_text = list()
stats_text.append(
_("{number} games played").format(number=us.games_played))
stats_text.append(
_("{number} first places").format(number=us.first_places))
stats_text.append(
_("{number} cards played").format(number=us.cards_played))
send_async(bot, update.message.chat_id,
text='\n'.join(stats_text))
dispatcher.add_handler(CommandHandler('help', help))
dispatcher.add_handler(CommandHandler('source', source))
dispatcher.add_handler(CommandHandler('news', news))
dispatcher.add_handler(CommandHandler('stats', stats))

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>

View File

@ -1,41 +0,0 @@
import unittest
from game import Game
from player import Player
class Test(unittest.TestCase):
game = None
def setUp(self):
self.game = Game()
def test_insert(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.assertEqual(p0, p2.next)
self.assertEqual(p1, p0.next)
self.assertEqual(p2, p1.next)
self.assertEqual(p0.prev, p2)
self.assertEqual(p1.prev, p0)
self.assertEqual(p2.prev, p1)
def test_reverse(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.game.reverse()
p3 = Player(self.game, "Player 3")
self.assertEqual(p0, p3.next)
self.assertEqual(p1, p2.next)
self.assertEqual(p2, p0.next)
self.assertEqual(p3, p1.next)
self.assertEqual(p0, p2.prev)
self.assertEqual(p1, p3.prev)
self.assertEqual(p2, p1.prev)
self.assertEqual(p3, p0.prev)

111
test/test_game_manager.py Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from telegram import User, Chat
from game_manager import GameManager
from errors import AlreadyJoinedError, LobbyClosedError, NoGameInChatError, \
NotEnoughPlayersError
class Test(unittest.TestCase):
game = None
def setUp(self):
self.gm = GameManager()
self.chat0 = Chat(0, 'group')
self.chat1 = Chat(1, 'group')
self.chat2 = Chat(2, 'group')
self.user0 = User(0, 'user0')
self.user1 = User(1, 'user1')
self.user2 = User(2, 'user2')
def test_new_game(self):
g0 = self.gm.new_game(self.chat0)
g1 = self.gm.new_game(self.chat1)
self.assertListEqual(self.gm.chatid_games[0], [g0])
self.assertListEqual(self.gm.chatid_games[1], [g1])
def test_join_game(self):
self.assertRaises(NoGameInChatError,
self.gm.join_game,
*(self.user0, self.chat0))
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.assertEqual(len(g0.players), 1)
self.gm.join_game(self.user1, self.chat0)
self.assertEqual(len(g0.players), 2)
g0.open = False
self.assertRaises(LobbyClosedError,
self.gm.join_game,
*(self.user2, self.chat0))
g0.open = True
self.assertRaises(AlreadyJoinedError,
self.gm.join_game,
*(self.user1, self.chat0))
def test_leave_game(self):
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.gm.join_game(self.user1, self.chat0)
self.assertRaises(NotEnoughPlayersError,
self.gm.leave_game,
*(self.user1, self.chat0))
self.gm.join_game(self.user2, self.chat0)
self.gm.leave_game(self.user0, self.chat0)
self.assertRaises(NoGameInChatError,
self.gm.leave_game,
*(self.user0, self.chat0))
def test_end_game(self):
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.gm.join_game(self.user1, self.chat0)
self.assertEqual(len(self.gm.userid_players[0]), 1)
g1 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user2, self.chat0)
self.gm.end_game(self.chat0, self.user0)
self.assertEqual(len(self.gm.chatid_games[0]), 1)
self.gm.end_game(self.chat0, self.user2)
self.assertFalse(0 in self.gm.chatid_games)
self.assertFalse(0 in self.gm.userid_players)
self.assertFalse(1 in self.gm.userid_players)
self.assertFalse(2 in self.gm.userid_players)

159
test/test_player.py Normal file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from game import Game
from player import Player
import card as c
class Test(unittest.TestCase):
game = None
def setUp(self):
self.game = Game(None)
def test_insert(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.assertEqual(p0, p2.next)
self.assertEqual(p1, p0.next)
self.assertEqual(p2, p1.next)
self.assertEqual(p0.prev, p2)
self.assertEqual(p1.prev, p0)
self.assertEqual(p2.prev, p1)
def test_reverse(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.game.reverse()
p3 = Player(self.game, "Player 3")
self.assertEqual(p0, p3.next)
self.assertEqual(p1, p2.next)
self.assertEqual(p2, p0.next)
self.assertEqual(p3, p1.next)
self.assertEqual(p0, p2.prev)
self.assertEqual(p1, p3.prev)
self.assertEqual(p2, p1.prev)
self.assertEqual(p3, p0.prev)
def test_leave(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
p1.leave()
self.assertEqual(p0, p2.next)
self.assertEqual(p2, p0.next)
def test_draw(self):
p = Player(self.game, "Player 0")
deck_before = len(self.game.deck.cards)
top_card = self.game.deck.cards[-1]
p.draw()
self.assertEqual(top_card, p.cards[-1])
self.assertEqual(deck_before, len(self.game.deck.cards) + 1)
def test_draw_two(self):
p = Player(self.game, "Player 0")
deck_before = len(self.game.deck.cards)
self.game.draw_counter = 2
p.draw()
self.assertEqual(deck_before, len(self.game.deck.cards) + 2)
def test_playable_cards_simple(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, '5')
p.cards = [c.Card(c.RED, '0'), c.Card(c.RED, '5'), c.Card(c.BLUE, '0'),
c.Card(c.GREEN, '5'), c.Card(c.GREEN, '8')]
expected = [c.Card(c.RED, '0'), c.Card(c.RED, '5'),
c.Card(c.GREEN, '5')]
self.assertListEqual(p.playable_cards(), expected)
def test_playable_cards_on_draw_two(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, c.DRAW_TWO)
self.game.draw_counter = 2
p.cards = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.RED, '5'),
c.Card(c.BLUE, '0'), c.Card(c.GREEN, '5'),
c.Card(c.GREEN, c.DRAW_TWO)]
expected = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.GREEN, c.DRAW_TWO)]
self.assertListEqual(p.playable_cards(), expected)
def test_playable_cards_on_draw_four(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, None, c.DRAW_FOUR)
self.game.draw_counter = 4
p.cards = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.RED, '5'),
c.Card(c.BLUE, '0'), c.Card(c.GREEN, '5'),
c.Card(c.GREEN, c.DRAW_TWO),
c.Card(None, None, c.DRAW_FOUR),
c.Card(None, None, c.CHOOSE)]
expected = list()
self.assertListEqual(p.playable_cards(), expected)
def test_bluffing(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, '1')
p.cards = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.RED, '5'),
c.Card(c.BLUE, '0'), c.Card(c.GREEN, '5'),
c.Card(c.RED, '5'), c.Card(c.GREEN, c.DRAW_TWO),
c.Card(None, None, c.DRAW_FOUR),
c.Card(None, None, c.CHOOSE)]
p.playable_cards()
self.assertTrue(p.bluffing)
p.cards = [c.Card(c.BLUE, '1'), c.Card(c.GREEN, '1'),
c.Card(c.GREEN, c.DRAW_TWO),
c.Card(None, None, c.DRAW_FOUR),
c.Card(None, None, c.CHOOSE)]
p.playable_cards()
self.assertFalse(p.bluffing)

32
user_setting.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from database import db, Optional, Required, PrimaryKey, db_session
class UserSetting(db.Entity):
id = PrimaryKey(int, auto=False, size=64) # Telegram User ID
lang = Optional(str, default='en') # The language setting for this user
stats = Optional(bool, default=False) # Opt-in to keep game statistics
first_places = Optional(int, default=0) # Nr. of games won in first place
games_played = Optional(int, default=0) # Nr. of games completed
cards_played = Optional(int, default=0) # Nr. of cards played total
use_keyboards = Optional(bool, default=False) # Use keyboards (unused)

186
utils.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Telegram bot to play UNO in group chats # Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de> # Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -17,7 +18,51 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from functools import wraps
from flufl.i18n import registry
from flufl.i18n import PackageStrategy
from telegram import Emoji from telegram import Emoji
from telegram.ext.dispatcher import run_async
import locales
from database import db_session
from user_setting import UserSetting
from shared_vars import gm
strategy = PackageStrategy('unobot', locales)
application = registry.register(strategy)
_ = application._
logger = logging.getLogger(__name__)
TIMEOUT = 2.5
def __(string, multi_translate):
"""Translates text into all locales on the stack"""
translations = list()
locales = list()
if not multi_translate:
_.push('en_US')
translations.append(_(string))
_.pop()
else:
while _.code:
translation = _(string)
if translation not in translations:
translations.append(translation)
locales.append(_.code)
_.pop()
for l in reversed(locales):
_.push(l)
return '\n'.join(translations)
def list_subtract(list1, list2): def list_subtract(list1, list2):
@ -30,22 +75,6 @@ def list_subtract(list1, list2):
return list(sorted(list1)) return list(sorted(list1))
def list_subtract_unsorted(list1, list2):
""" Helper function to subtract two lists and return the sorted result """
list1 = list1.copy()
for x in list2:
try:
list1.remove(x)
except ValueError:
print(list1)
print(list2)
print(x)
raise
return list1
def display_name(user): def display_name(user):
""" Get the current players name including their username, if possible """ """ Get the current players name including their username, if possible """
user_name = user.first_name user_name = user.first_name
@ -57,10 +86,127 @@ def display_name(user):
def display_color(color): def display_color(color):
""" Convert a color code to actual color name """ """ Convert a color code to actual color name """
if color == "r": if color == "r":
return Emoji.HEAVY_BLACK_HEART + " Red" return _("{emoji} Red").format(emoji=Emoji.HEAVY_BLACK_HEART)
if color == "b": if color == "b":
return Emoji.BLUE_HEART + " Blue" return _("{emoji} Blue").format(emoji=Emoji.BLUE_HEART)
if color == "g": if color == "g":
return Emoji.GREEN_HEART + " Green" return _("{emoji} Green").format(emoji=Emoji.GREEN_HEART)
if color == "y": if color == "y":
return Emoji.YELLOW_HEART + " Yellow" return _("{emoji} Yellow").format(emoji=Emoji.YELLOW_HEART)
def display_color_group(color, game):
""" Convert a color code to actual color name """
if color == "r":
return __("{emoji} Red", game.translate).format(
emoji=Emoji.HEAVY_BLACK_HEART)
if color == "b":
return __("{emoji} Blue", game.translate).format(
emoji=Emoji.BLUE_HEART)
if color == "g":
return __("{emoji} Green", game.translate).format(
emoji=Emoji.GREEN_HEART)
if color == "y":
return __("{emoji} Yellow", game.translate).format(
emoji=Emoji.YELLOW_HEART)
def error(bot, update, error):
"""Simple error handler"""
logger.exception(error)
@run_async
def send_async(bot, *args, **kwargs):
"""Send a message asynchronously"""
if 'timeout' not in kwargs:
kwargs['timeout'] = TIMEOUT
try:
bot.sendMessage(*args, **kwargs)
except Exception as e:
error(None, None, e)
@run_async
def answer_async(bot, *args, **kwargs):
"""Answer an inline query asynchronously"""
if 'timeout' not in kwargs:
kwargs['timeout'] = TIMEOUT
try:
bot.answerInlineQuery(*args, **kwargs)
except Exception as e:
error(None, None, e)
def user_locale(func):
@wraps(func)
@db_session
def wrapped(bot, update, *pargs, **kwargs):
user, chat = _user_chat_from_update(update)
with db_session:
us = UserSetting.get(id=user.id)
if us:
_.push(us.lang)
else:
_.push('en_US')
result = func(bot, update, *pargs, **kwargs)
_.pop()
return result
return wrapped
def game_locales(func):
@wraps(func)
@db_session
def wrapped(bot, update, *pargs, **kwargs):
user, chat = _user_chat_from_update(update)
player = gm.player_for_user_in_chat(user, chat)
locales = list()
if player:
for player in player.game.players:
us = UserSetting.get(id=player.user.id)
if us:
loc = us.lang
else:
loc = 'en_US'
if loc in locales:
continue
_.push(loc)
locales.append(loc)
result = func(bot, update, *pargs, **kwargs)
for i in locales:
_.pop()
return result
return wrapped
def _user_chat_from_update(update):
try:
user = update.message.from_user
chat = update.message.chat
except (NameError, AttributeError):
try:
user = update.inline_query.from_user
chat = gm.userid_current[user.id].game.chat
except KeyError:
chat = None
except (NameError, AttributeError):
try:
user = update.chosen_inline_result.from_user
chat = gm.userid_current[user.id].game.chat
except (NameError, AttributeError):
chat = None
return user, chat