Merge pull request #8 from jh0ker/fix-code

Fix code
This commit is contained in:
Jannes Höke 2016-05-22 19:24:59 +02:00
commit c477701b75
24 changed files with 2269 additions and 513 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

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`

768
bot.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>
@ -23,170 +24,130 @@ from random import randint
from telegram import ParseMode, Message, Chat, InlineKeyboardMarkup, \ from telegram import ParseMode, Message, Chat, InlineKeyboardMarkup, \
InlineKeyboardButton InlineKeyboardButton
from telegram.ext import Updater, InlineQueryHandler, \ from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \
ChosenInlineResultHandler, CommandHandler, MessageHandler, Filters, \ CommandHandler, MessageHandler, Filters, CallbackQueryHandler
CallbackQueryHandler
from telegram.ext.dispatcher import run_async from telegram.ext.dispatcher import run_async
from telegram.utils.botan import Botan
from game_manager import GameManager
from credentials import TOKEN, BOTAN_TOKEN
from start_bot import start_bot from start_bot import start_bot
from results import * from results import (add_call_bluff, add_choose_color, add_draw, add_gameinfo,
from utils import * add_no_game, add_not_started, add_other_cards, add_pass,
add_card)
from user_setting import UserSetting
from utils import display_name
import card as c
from errors import (NoGameInChatError, LobbyClosedError, AlreadyJoinedError,
NotEnoughPlayersError, DeckEmptyError)
from utils import _, __, send_async, answer_async, user_locale, game_locales, \
error, TIMEOUT
from shared_vars import botan, gm, updater, dispatcher
import simple_commands, settings
from simple_commands import help
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG) level=logging.DEBUG)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
gm = GameManager()
u = Updater(token=TOKEN, workers=32)
dp = u.dispatcher
botan = False
if BOTAN_TOKEN:
botan = Botan(BOTAN_TOKEN)
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"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\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.\n")
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")
@run_async
def send_async(bot, *args, **kwargs):
if 'timeout' not in kwargs:
kwargs['timeout'] = 2.5
try:
bot.sendMessage(*args, **kwargs)
except Exception as e:
error(None, None, e)
@run_async
def answer_async(bot, *args, **kwargs):
if 'timeout' not in kwargs:
kwargs['timeout'] = 2.5
try:
bot.answerInlineQuery(*args, **kwargs)
except Exception as e:
error(None, None, e)
def error(bot, update, error):
""" Simple error handler """
logger.exception(error)
@user_locale
def new_game(bot, update): def new_game(bot, update):
""" Handler for the /new command """ """Handler for the /new command"""
chat_id = update.message.chat_id chat_id = update.message.chat_id
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help(bot, update) help(bot, update)
else: else:
game = gm.new_game(update.message.chat) game = gm.new_game(update.message.chat)
game.owner = update.message.from_user game.owner = update.message.from_user
send_async(bot, chat_id, send_async(bot, chat_id,
text="Created a new game! Join the game with /join " text=_("Created a new game! Join the game with /join "
"and start the game with /start") "and start the game with /start"))
if botan: if botan:
botan.track(update.message, 'New games') botan.track(update.message, 'New games')
@user_locale
def join_game(bot, update): def join_game(bot, update):
""" Handler for the /join command """ """Handler for the /join command"""
chat_id = update.message.chat_id chat = update.message.chat
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help(bot, update) help(bot, update)
return
try:
gm.join_game(update.message.from_user, chat)
except LobbyClosedError:
send_async(bot, chat.id, text=_("The lobby is closed"))
except NoGameInChatError:
send_async(bot, chat.id,
text=_("No game is running at the moment. "
"Create a new game with /new"),
reply_to_message_id=update.message.message_id)
except AlreadyJoinedError:
send_async(bot, chat.id,
text=_("You already joined the game. Start the game "
"with /start"),
reply_to_message_id=update.message.message_id)
except DeckEmptyError:
send_async(bot, chat.id,
text=_("There are not enough cards left in the deck for "
"new players to join."),
reply_to_message_id=update.message.message_id)
else: else:
try: send_async(bot, chat.id,
game = gm.chatid_games[chat_id][-1] text=_("Joined the game"),
if not game.open: reply_to_message_id=update.message.message_id)
send_async(bot, chat_id, text="The lobby is closed")
return
except (KeyError, IndexError):
pass
joined = gm.join_game(chat_id, update.message.from_user)
if joined:
send_async(bot, chat_id,
text="Joined the game",
reply_to_message_id=update.message.message_id)
elif joined is None:
send_async(bot, chat_id,
text="No game is running at the moment. "
"Create a new game with /new",
reply_to_message_id=update.message.message_id)
else:
send_async(bot, chat_id,
text="You already joined the game. Start the game "
"with /start",
reply_to_message_id=update.message.message_id)
@user_locale
def leave_game(bot, update): def leave_game(bot, update):
""" Handler for the /leave command """ """Handler for the /leave command"""
chat_id = update.message.chat_id chat = update.message.chat
user = update.message.from_user user = update.message.from_user
players = gm.userid_players.get(user.id, list())
for player in players: player = gm.player_for_user_in_chat(user, chat)
if player.game.chat.id == chat_id:
game = player.game if player is None:
break send_async(bot, chat.id, text=_("You are not playing in a game in "
else: "this group."),
send_async(bot, chat_id, text="You are not playing in a game in "
"this group.",
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
game = player.game
user = update.message.from_user user = update.message.from_user
if len(game.players) < 3: try:
gm.end_game(chat_id, user) gm.leave_game(user, chat)
send_async(bot, chat_id, text="Game ended!")
except NoGameInChatError:
send_async(bot, chat.id, text=_("You are not playing in a game in "
"this group."),
reply_to_message_id=update.message.message_id)
except NotEnoughPlayersError:
gm.end_game(chat, user)
send_async(bot, chat.id, text=__("Game ended!", game.translate))
else: else:
if gm.leave_game(user, chat_id): send_async(bot, chat.id,
send_async(bot, chat_id, text=__("Okay. Next Player: {name}", game.translate).format(
text="Okay. Next Player: " + name=display_name(game.current_player.user)),
display_name(game.current_player.user), reply_to_message_id=update.message.message_id)
reply_to_message_id=update.message.message_id)
else:
send_async(bot, chat_id, text="You are not playing in a game in "
"this group.",
reply_to_message_id=update.message.message_id)
@run_async
def select_game(bot, update): def select_game(bot, update):
"""Handler for callback queries to select the current game"""
chat_id = int(update.callback_query.data) chat_id = int(update.callback_query.data)
user_id = update.callback_query.from_user.id user_id = update.callback_query.from_user.id
@ -196,225 +157,307 @@ def select_game(bot, update):
gm.userid_current[user_id] = player gm.userid_current[user_id] = player
break break
else: else:
send_async(bot, update.callback_query.message.chat_id, bot.sendMessage(update.callback_query.message.chat_id,
text="Game not found :(") text=_("Game not found."),
timeout=TIMEOUT)
return return
back = [[InlineKeyboardButton(text='Back to last group', back = [[InlineKeyboardButton(text=_("Back to last group"),
switch_inline_query='')]] switch_inline_query='')]]
bot.answerCallbackQuery(update.callback_query.id, bot.answerCallbackQuery(update.callback_query.id,
text="Please switch to the group you selected!", text=_("Please switch to the group you selected!"),
show_alert=False, show_alert=False,
timeout=2.5) timeout=TIMEOUT)
bot.editMessageText(chat_id=update.callback_query.message.chat_id, bot.editMessageText(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id, message_id=update.callback_query.message.message_id,
text="Selected group: %s\n" text=_("Selected group: {group}\n"
"<b>Make sure that you switch to the correct " "<b>Make sure that you switch to the correct "
"group!</b>" "group!</b>").format(
% gm.userid_current[user_id].game.chat.title, group=gm.userid_current[user_id].game.chat.title),
reply_markup=InlineKeyboardMarkup(back), reply_markup=InlineKeyboardMarkup(back),
parse_mode=ParseMode.HTML, parse_mode=ParseMode.HTML,
timeout=2.5) timeout=TIMEOUT)
@game_locales
def status_update(bot, update): def status_update(bot, update):
""" Remove player from game if user leaves the group """ """Remove player from game if user leaves the group"""
chat = update.message.chat
if update.message.left_chat_member: if update.message.left_chat_member:
user = update.message.left_chat_member
try: try:
chat_id = update.message.chat_id gm.leave_game(user, chat)
user = update.message.left_chat_member game = gm.player_for_user_in_chat(user, chat).game
except KeyError:
return
if gm.leave_game(user, chat_id): except NoGameInChatError:
send_async(bot, chat_id, text="Removing %s from the game" pass
% display_name(user)) except NotEnoughPlayersError:
gm.end_game(chat, user)
send_async(bot, chat.id, text=__("Game ended!", game.translate))
else:
send_async(bot, chat.id, text=__("Removing {name} from the game",
game.translate)
.format(name=display_name(user)))
@game_locales
@user_locale
def start_game(bot, update, args): def start_game(bot, update, args):
""" Handler for the /start command """ """Handler for the /start command"""
if update.message.chat.type != 'private': if update.message.chat.type != 'private':
# Show the first card chat = update.message.chat
chat_id = update.message.chat_id
try: try:
game = gm.chatid_games[chat_id][-1] game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
send_async(bot, chat_id, text="There is no game running in this " send_async(bot, chat.id,
"chat. Create a new one with /new") text=_("There is no game running in this chat. Create "
"a new one with /new"))
return return
if game.current_player is None or \ if game.started:
game.current_player is game.current_player.next: send_async(bot, chat.id, text=_("The game has already started"))
send_async(bot, chat_id, text="At least two players must /join "
"the game before you can start it") elif len(game.players) < 2:
elif game.started: send_async(bot, chat.id,
send_async(bot, chat_id, text="The game has already started") text=_("At least two players must /join the game "
"before you can start it"))
else: else:
game.play_card(game.last_card) game.play_card(game.last_card)
game.started = True game.started = True
bot.sendSticker(chat_id,
sticker=c.STICKERS[str(game.last_card)], first_message = (
timeout=2.5) __("First player: {name}\n"
send_async(bot, chat_id, "Use /close to stop people from joining the game.\n"
text="First player: %s\n" "Enable multi-translations with /enable_translations",
"Use /close to stop people from joining the game." game.translate)
% display_name(game.current_player.user)) .format(name=display_name(game.current_player.user)))
@run_async
def send_first():
"""Send the first card and player"""
bot.sendSticker(chat.id,
sticker=c.STICKERS[str(game.last_card)],
timeout=TIMEOUT)
bot.sendMessage(chat.id,
text=first_message,
timeout=TIMEOUT)
send_first()
elif len(args) and args[0] == 'select': elif len(args) and args[0] == 'select':
players = gm.userid_players[update.message.from_user.id] players = gm.userid_players[update.message.from_user.id]
groups = list() groups = list()
for player in players: for player in players:
groups.append([InlineKeyboardButton(text=player.game.chat.title, title = player.game.chat.title
callback_data=
str(player.game.chat.id))]) if player is gm.userid_current[update.message.from_user.id]:
title = '- %s -' % player.game.chat.title
groups.append(
[InlineKeyboardButton(text=title,
callback_data=str(player.game.chat.id))]
)
send_async(bot, update.message.chat_id, send_async(bot, update.message.chat_id,
text='Please select the group you want to play in. ', text=_('Please select the group you want to play in.'),
reply_markup=InlineKeyboardMarkup(groups)) reply_markup=InlineKeyboardMarkup(groups))
else: else:
help(bot, update) help(bot, update)
@user_locale
def close_game(bot, update): def close_game(bot, update):
""" Handler for the /close command """ """Handler for the /close command"""
chat_id = update.message.chat_id chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat_id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(bot, chat_id, text="There is no running game") send_async(bot, chat.id,
text=_("There is no running game in this chat."))
return return
game = games[-1] game = games[-1]
if game.owner.id == user.id: if game.owner.id == user.id:
game.open = False game.open = False
send_async(bot, chat_id, text="Closed the lobby. " send_async(bot, chat.id, text=_("Closed the lobby. "
"No more players can join this game.") "No more players can join this game."))
return return
else: else:
send_async(bot, chat_id, send_async(bot, chat.id,
text="Only the game creator (%s) can do that" text=_("Only the game creator ({name}) can do that")
% game.owner.first_name, .format(name=game.owner.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@user_locale
def open_game(bot, update): def open_game(bot, update):
""" Handler for the /open command """ """Handler for the /open command"""
chat_id = update.message.chat_id chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat_id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(bot, chat_id, text="There is no running game") send_async(bot, chat.id,
text=_("There is no running game in this chat."))
return return
game = games[-1] game = games[-1]
if game.owner.id == user.id: if game.owner.id == user.id:
game.open = True game.open = True
send_async(bot, chat_id, text="Opened the lobby. " send_async(bot, chat.id, text=_("Opened the lobby. "
"New players may /join the game.") "New players may /join the game."))
return return
else: else:
send_async(bot, chat_id, send_async(bot, chat.id,
text="Only the game creator (%s) can do that" text=_("Only the game creator ({name}) can do that")
% game.owner.first_name, .format(name=game.owner.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
def skip_player(bot, update): @user_locale
""" Handler for the /skip command """ def enable_translations(bot, update):
chat_id = update.message.chat_id """Handler for the /enable_translations command"""
chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat_id) games = gm.chatid_games.get(chat.id)
players = gm.userid_players.get(user.id)
if not games: if not games:
send_async(bot, chat_id, text="There is no running game") send_async(bot, chat.id,
text=_("There is no running game in this chat."))
return return
if not players: game = games[-1]
send_async(bot, chat_id, text="You are not playing")
if game.owner.id == user.id:
game.translate = True
send_async(bot, chat.id, text=_("Enabled multi-translations. "
"Disable with /disable_translations"))
return return
for game in games: else:
for player in players: send_async(bot, chat.id,
if player in game.players: text=_("Only the game creator ({name}) can do that")
started = game.current_player.turn_started .format(name=game.owner.first_name),
now = datetime.now() reply_to_message_id=update.message.message_id)
delta = (now - started).seconds return
if delta < game.current_player.waiting_time:
send_async(bot, chat_id,
text="Please wait %d seconds"
% (game.current_player.waiting_time -
delta),
reply_to_message_id=
update.message.message_id)
return
elif game.current_player.waiting_time > 0:
game.current_player.anti_cheat += 1
game.current_player.waiting_time -= 30
game.current_player.cards.append(game.deck.draw())
send_async(bot, chat_id,
text="Waiting time to skip this player has "
"been reduced to %d seconds.\n"
"Next player: %s"
% (game.current_player.waiting_time,
display_name(
game.current_player.next.user)))
game.turn()
return
elif len(game.players) > 2:
send_async(bot, chat_id,
text="%s was skipped four times in a row "
"and has been removed from the game.\n"
"Next player: %s"
% (display_name(game.current_player.user),
display_name(
game.current_player.next.user)))
gm.leave_game(game.current_player.user, chat_id)
return
else:
send_async(bot, chat_id,
text="%s was skipped four times in a row "
"and has been removed from the game.\n"
"The game ended."
% display_name(game.current_player.user))
gm.end_game(chat_id, game.current_player.user)
return
def help(bot, update): @user_locale
""" Handler for the /help command """ def disable_translations(bot, update):
send_async(bot, update.message.chat_id, text=help_text, """Handler for the /disable_translations command"""
parse_mode=ParseMode.HTML, disable_web_page_preview=True) chat = update.message.chat
user = update.message.from_user
games = gm.chatid_games.get(chat.id)
if not games:
send_async(bot, chat.id,
text=_("There is no running game in this chat."))
return
game = games[-1]
if game.owner.id == user.id:
game.translate = False
send_async(bot, chat.id, text=_("Disabled multi-translations. "
"Enable them again with "
"/enable_translations"))
return
else:
send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that")
.format(name=game.owner.first_name),
reply_to_message_id=update.message.message_id)
return
def source(bot, update): @game_locales
""" Handler for the /help command """ @user_locale
send_async(bot, update.message.chat_id, text=source_text, def skip_player(bot, update):
parse_mode=ParseMode.HTML, disable_web_page_preview=True) """Handler for the /skip command"""
chat = update.message.chat
user = update.message.from_user
def news(bot, update):
""" Handler for the /news command """ player = gm.player_for_user_in_chat(user, chat)
send_async(bot, update.message.chat_id, if not player:
text="All news here: https://telegram.me/unobotupdates", send_async(bot, chat.id,
disable_web_page_preview=True) text=_("You are not playing in a game in this chat."))
return
game = player.game
skipped_player = game.current_player
next_player = game.current_player.next
started = skipped_player.turn_started
now = datetime.now()
delta = (now - started).seconds
if delta < skipped_player.waiting_time:
send_async(bot, chat.id,
text=_("Please wait {time} seconds")
.format(time=(skipped_player.waiting_time - delta)),
reply_to_message_id=update.message.message_id)
elif skipped_player.waiting_time > 0:
skipped_player.anti_cheat += 1
skipped_player.waiting_time -= 30
try:
skipped_player.draw()
except DeckEmptyError:
pass
send_async(bot, chat.id,
text=__("Waiting time to skip this player has "
"been reduced to {time} seconds.\n"
"Next player: {name}", game.translate)
.format(time=skipped_player.waiting_time,
name=display_name(next_player.user)))
game.turn()
else:
try:
gm.leave_game(skipped_player.user, chat)
send_async(bot, chat.id,
text=__("{name1} was skipped four times in a row "
"and has been removed from the game.\n"
"Next player: {name2}", game.translate)
.format(name1=display_name(skipped_player.user),
name2=display_name(next_player.user)))
except NotEnoughPlayersError:
send_async(bot, chat.id,
text=__("{name} was skipped four times in a row "
"and has been removed from the game.\n"
"The game ended.", game.translate)
.format(name=display_name(skipped_player.user)))
gm.end_game(chat.id, skipped_player.user)
@game_locales
@user_locale
def reply_to_query(bot, update): def reply_to_query(bot, update):
""" Builds the result list for inline queries and answers to the client """ """
Handler for inline queries.
Builds the result list for inline queries and answers to the client.
"""
results = list() results = list()
playable = list() playable = list()
switch = None switch = None
@ -429,33 +472,34 @@ def reply_to_query(bot, update):
else: else:
if not game.started: if not game.started:
add_not_started(results) add_not_started(results)
elif user_id == game.current_player.user.id: elif user_id == game.current_player.user.id:
if game.choosing_color: if game.choosing_color:
add_choose_color(results) add_choose_color(results, game)
add_other_cards(playable, player, results, game)
else: else:
if not player.drew: if not player.drew:
add_draw(player, results) add_draw(player, results)
else: else:
add_pass(results) add_pass(results, game)
if game.last_card.special == c.DRAW_FOUR and game.draw_counter: if game.last_card.special == c.DRAW_FOUR and game.draw_counter:
add_call_bluff(results) add_call_bluff(results, game)
playable = player.playable_cards() playable = player.playable_cards()
added_ids = list() added_ids = list() # Duplicates are not allowed
for card in sorted(player.cards): for card in sorted(player.cards):
add_play_card(game, card, results, add_card(game, card, results,
can_play=(card in playable and can_play=(card in playable and
str(card) not in added_ids)) str(card) not in added_ids))
added_ids.append(str(card)) added_ids.append(str(card))
if False or game.choosing_color:
add_other_cards(playable, player, results, game)
elif user_id != game.current_player.user.id or not game.started: elif user_id != game.current_player.user.id or not game.started:
for card in sorted(player.cards): for card in sorted(player.cards):
add_play_card(game, card, results, can_play=False) add_card(game, card, results, can_play=False)
else: else:
add_gameinfo(game, results) add_gameinfo(game, results)
@ -463,20 +507,25 @@ def reply_to_query(bot, update):
result.id += ':%d' % player.anti_cheat result.id += ':%d' % player.anti_cheat
if players and game and len(players) > 1: if players and game and len(players) > 1:
switch = 'Current game: %s' % game.chat.title switch = _('Current game: {game}').format(game=game.chat.title)
answer_async(bot, update.inline_query.id, results, cache_time=0, answer_async(bot, update.inline_query.id, results, cache_time=0,
switch_pm_text=switch, switch_pm_parameter='select') switch_pm_text=switch, switch_pm_parameter='select')
@game_locales
@user_locale
def process_result(bot, update): def process_result(bot, update):
""" Check the players actions and act accordingly """ """
Handler for chosen inline results.
Checks the players actions and acts accordingly.
"""
try: try:
user = update.chosen_inline_result.from_user user = update.chosen_inline_result.from_user
player = gm.userid_current[user.id] player = gm.userid_current[user.id]
game = player.game game = player.game
result_id = update.chosen_inline_result.result_id result_id = update.chosen_inline_result.result_id
chat_id = game.chat.id chat = game.chat
except KeyError: except KeyError:
return return
@ -491,103 +540,160 @@ def process_result(bot, update):
elif len(result_id) == 36: # UUID result elif len(result_id) == 36: # UUID result
return return
elif int(anti_cheat) != last_anti_cheat: elif int(anti_cheat) != last_anti_cheat:
send_async(bot, chat_id, send_async(bot, chat.id,
text="Cheat attempt by %s" % display_name(player.user)) text=__("Cheat attempt by {name}", game.translate)
.format(name=display_name(player.user)))
return return
elif result_id == 'call_bluff': elif result_id == 'call_bluff':
reset_waiting_time(bot, chat_id, player) reset_waiting_time(bot, player)
do_call_bluff(bot, chat_id, game, player) do_call_bluff(bot, player)
elif result_id == 'draw': elif result_id == 'draw':
reset_waiting_time(bot, chat_id, player) reset_waiting_time(bot, player)
do_draw(game, player) do_draw(bot, player)
elif result_id == 'pass': elif result_id == 'pass':
game.turn() game.turn()
elif result_id in c.COLORS: elif result_id in c.COLORS:
game.choose_color(result_id) game.choose_color(result_id)
else: else:
reset_waiting_time(bot, chat_id, player) reset_waiting_time(bot, player)
do_play_card(bot, chat_id, game, player, result_id, user) do_play_card(bot, player, result_id)
if game in gm.chatid_games.get(chat_id, list()): if game in gm.chatid_games.get(chat.id, list()):
send_async(bot, chat_id, text="Next player: " + send_async(bot, chat.id,
display_name(game.current_player.user)) text=__("Next player: {name}", game.translate)
.format(name=display_name(game.current_player.user)))
def reset_waiting_time(bot, chat_id, player): def reset_waiting_time(bot, player):
"""Resets waiting time for a player and sends a notice to the group"""
chat = player.game.chat
if player.waiting_time < 90: if player.waiting_time < 90:
player.waiting_time = 90 player.waiting_time = 90
send_async(bot, chat_id, text="Waiting time for %s has been reset to " send_async(bot, chat.id,
"90 seconds" % display_name(player.user)) text=__("Waiting time for {name} has been reset to 90 "
"seconds", player.game.translate)
.format(name=display_name(player.user)))
def do_play_card(bot, chat_id, game, player, result_id, user): def do_play_card(bot, player, result_id):
"""Plays the selected card and sends an update to the group if needed"""
card = c.from_str(result_id) card = c.from_str(result_id)
game.play_card(card) player.play(card)
player.cards.remove(card) game = player.game
chat = game.chat
user = player.user
us = UserSetting.get(id=user.id)
if not us:
us = UserSetting(id=user.id)
us.cards_played += 1
if game.choosing_color: if game.choosing_color:
send_async(bot, chat_id, text="Please choose a color") send_async(bot, chat.id, text=_("Please choose a color"))
if len(player.cards) == 1: if len(player.cards) == 1:
send_async(bot, chat_id, text="UNO!") send_async(bot, chat.id, text="UNO!")
if len(player.cards) == 0: if len(player.cards) == 0:
send_async(bot, chat_id, text="%s won!" % user.first_name) send_async(bot, chat.id,
if len(game.players) < 3: text=__("{name} won!", game.translate)
send_async(bot, chat_id, text="Game ended!") .format(name=user.first_name))
gm.end_game(chat_id, user)
else: if us.stats:
gm.leave_game(user, chat_id) us.games_played += 1
if game.players_won is 0:
us.first_places += 1
try:
gm.leave_game(user, chat)
except NotEnoughPlayersError:
send_async(bot, chat.id, text=__("Game ended!", game.translate))
us2 = UserSetting.get(id=game.current_player.next.user.id)
if us2 and us2.stats:
us2.games_played += 1
gm.end_game(chat, user)
if botan: if botan:
botan.track(Message(randint(1, 1000000000), user, datetime.now(), botan.track(Message(randint(1, 1000000000), user, datetime.now(),
Chat(chat_id, 'group')), Chat(chat.id, 'group')),
'Played cards') 'Played cards')
def do_draw(game, player): def do_draw(bot, player):
"""Does the drawing"""
game = player.game
draw_counter_before = game.draw_counter draw_counter_before = game.draw_counter
for n in range(game.draw_counter or 1):
player.cards.append(game.deck.draw()) try:
game.draw_counter = 0 player.draw()
player.drew = True except DeckEmptyError:
send_async(bot, player.game.chat.id,
text=__("There are no more cards in the deck.",
game.translate))
if (game.last_card.value == c.DRAW_TWO or if (game.last_card.value == c.DRAW_TWO or
game.last_card.special == c.DRAW_FOUR) and \ game.last_card.special == c.DRAW_FOUR) and \
draw_counter_before > 0: draw_counter_before > 0:
game.turn() game.turn()
def do_call_bluff(bot, chat_id, game, player): def do_call_bluff(bot, player):
"""Handles the bluff calling"""
game = player.game
chat = game.chat
if player.prev.bluffing: if player.prev.bluffing:
send_async(bot, chat_id, text="Bluff called! Giving %d cards to %s" send_async(bot, chat.id,
% (game.draw_counter, text=__("Bluff called! Giving 4 cards to {name}",
player.prev.user.first_name)) game.translate)
for i in range(game.draw_counter): .format(name=player.prev.user.first_name))
player.prev.cards.append(game.deck.draw())
try:
player.prev.draw()
except DeckEmptyError:
send_async(bot, player.game.chat.id,
text=__("There are no more cards in the deck.",
game.translate))
else: else:
send_async(bot, chat_id, text="%s didn't bluff! Giving %d cards to %s" game.draw_counter += 2
% (player.prev.user.first_name, send_async(bot, chat.id,
game.draw_counter + 2, text=__("{name1} didn't bluff! Giving 6 cards to {name2}",
player.user.first_name)) game.translate)
for i in range(game.draw_counter + 2): .format(name1=player.prev.user.first_name,
player.cards.append(game.deck.draw()) name2=player.user.first_name))
game.draw_counter = 0 try:
player.draw()
except DeckEmptyError:
send_async(bot, player.game.chat.id,
text=__("There are no more cards in the deck.",
game.translate))
game.turn() game.turn()
# Add all handlers to the dispatcher and run the bot # Add all handlers to the dispatcher and run the bot
dp.addHandler(InlineQueryHandler(reply_to_query)) dispatcher.add_handler(InlineQueryHandler(reply_to_query))
dp.addHandler(ChosenInlineResultHandler(process_result)) dispatcher.add_handler(ChosenInlineResultHandler(process_result))
dp.addHandler(CallbackQueryHandler(select_game)) dispatcher.add_handler(CallbackQueryHandler(select_game))
dp.addHandler(CommandHandler('start', start_game, pass_args=True)) dispatcher.add_handler(CommandHandler('start', start_game, pass_args=True))
dp.addHandler(CommandHandler('new', new_game)) dispatcher.add_handler(CommandHandler('new', new_game))
dp.addHandler(CommandHandler('join', join_game)) dispatcher.add_handler(CommandHandler('join', join_game))
dp.addHandler(CommandHandler('leave', leave_game)) dispatcher.add_handler(CommandHandler('leave', leave_game))
dp.addHandler(CommandHandler('open', open_game)) dispatcher.add_handler(CommandHandler('open', open_game))
dp.addHandler(CommandHandler('close', close_game)) dispatcher.add_handler(CommandHandler('close', close_game))
dp.addHandler(CommandHandler('skip', skip_player)) dispatcher.add_handler(CommandHandler('enable_translations',
dp.addHandler(CommandHandler('help', help)) enable_translations))
dp.addHandler(CommandHandler('source', source)) dispatcher.add_handler(CommandHandler('disable_translations',
dp.addHandler(CommandHandler('news', news)) disable_translations))
dp.addHandler(MessageHandler([Filters.status_update], status_update)) dispatcher.add_handler(CommandHandler('skip', skip_player))
dp.addErrorHandler(error) dispatcher.add_handler(MessageHandler([Filters.status_update], status_update))
dispatcher.add_error_handler(error)
start_bot(u) start_bot(updater)
u.idle() updater.idle()

11
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>
@ -180,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
@ -205,16 +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"""
return str(self) == str(other) return str(self) == str(other)
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()

22
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,22 +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()
self.logger.debug("Drawing card " + str(card)) self.logger.debug("Drawing card " + str(card))
return card return card
except IndexError: except IndexError:
while len(self.graveyard): if len(self.graveyard):
self.cards.append(self.graveyard.pop()) while len(self.graveyard):
self.shuffle() self.cards.append(self.graveyard.pop())
return self.draw() self.shuffle()
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"""
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.dismiss(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,76 +68,85 @@ 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 player is game.current_player:
game.turn()
player.leave() if not player:
players.remove(player) raise NoGameInChatError
# If this is the selected game, switch to another game = player.game
if self.userid_current[user.id] is player:
if len(players): if len(game.players) < 3:
self.userid_current[user.id] = players[0] raise NotEnoughPlayersError()
else:
del self.userid_current[user.id] if player is game.current_player:
return True game.turn()
player.leave()
players.remove(player)
# If this is the selected game, switch to another
if self.userid_current.get(user.id, None) is player:
if players:
self.userid_current[user.id] = players[0]
else: else:
return False del self.userid_current[user.id]
del self.userid_players[user.id]
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:
this_users_players = self.userid_players[player.user.id] raise NoGameInChatError
this_users_players.remove(player)
if len(this_users_players) is 0: game = player.game
del self.userid_players[player.user.id]
del self.userid_current[player.user.id] # Clear game
else: for player_in_game in game.players:
this_users_players = self.userid_players[player_in_game.user.id]
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"

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>
@ -21,6 +22,7 @@ import logging
from datetime import datetime from datetime import datetime
import card as c import card as c
from errors import DeckEmptyError
class Player(object): class Player(object):
@ -37,6 +39,15 @@ class Player(object):
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
@ -48,9 +59,6 @@ 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
@ -58,7 +66,7 @@ class Player(object):
self.waiting_time = 90 self.waiting_time = 90
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
@ -100,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
@ -115,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)
@ -127,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
@ -149,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))
)

105
settings.py Normal file
View file

@ -0,0 +1,105 @@
#!/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']]
@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)

170
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):
@ -41,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