From ea881cf5200eee8d4168c13109d8b6f37969bfe9 Mon Sep 17 00:00:00 2001 From: Jarv Date: Mon, 27 Nov 2017 17:59:19 +0100 Subject: [PATCH] Added gamemodes. New mode: Fast. (#44) * Changes for possible bug * New sanic mode wip * More changes for fast mode, WIP * Fast mode is playable (code is ugly tho) * Fixed skip error * Fixed fast mode error * Bug fixing * Possible fix for the /leave bug before the game starts * Update README to include Codacy badge * Fixing error prone code * Removing code smells * Removing more code smells * How long can this go on? (More smells according to Codacy) * Compile locale fixed for Linux. Small es_ES fix. * Major refactoring * Wild mode finished. Changed emojis for text in log. * Removing test prints, back to emojis * Code cleaning and fix for player time in fast mode * Changing help to not override builtin function * Decreased bot.py's complexity * Default gamemode is now Fast. Added a bot configuration file * back to random * Moved logger to shared_vars * Added MIN_FAST_TURN_TIME to config and fixed 'skipped 4 times' message * Pull review changes * More review changes * Removing codacy badge linked to my account for pull request * Fixed first special card issue, logger back to how it was (with just one logging init) * Renamed gameplay config file to gameplay_config.py. --- README.md | 2 + actions.py | 219 +++++++++++++++++++++++ bot.py | 267 ++++++++-------------------- card.py | 1 + database.py | 2 +- deck.py | 36 ++-- game.py | 34 +++- game_manager.py | 29 +-- gameplay_config.py | 6 + internationalization.py | 4 +- locales/compile.sh | 2 +- locales/es_ES/LC_MESSAGES/unobot.po | 10 +- player.py | 24 +-- results.py | 36 ++++ shared_vars.py | 3 +- simple_commands.py | 18 +- test/test_game_manager.py | 6 +- user_setting.py | 3 +- utils.py | 17 ++ 19 files changed, 472 insertions(+), 247 deletions(-) create mode 100644 actions.py create mode 100644 gameplay_config.py diff --git a/README.md b/README.md index 3dfe7e6..db45387 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ To run the bot yourself, you will need: - Use `/setinline` and `/setinlinefeedback` with BotFather for your bot. - Install requirements (using a `virtualenv` is recommended): `pip install -r requirements.txt` +You can change some gameplay parameters like turn times, minimum amount of players and default gamemode in `gameplay_config.py`. Check the gamemodes available with the `/modes` command. + Then run the bot with `python3 bot.py`. Code documentation is minimal but there. diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..958e755 --- /dev/null +++ b/actions.py @@ -0,0 +1,219 @@ +import random + +import logging + +import card as c +from datetime import datetime + +from telegram import Message, Chat + +from gameplay_config import TIME_REMOVAL_AFTER_SKIP, MIN_FAST_TURN_TIME +from errors import DeckEmptyError, NotEnoughPlayersError +from internationalization import __, _ +from shared_vars import gm, botan +from user_setting import UserSetting +from utils import send_async, display_name, game_is_running + +logger = logging.getLogger(__name__) + +class Countdown(object): + player = None + job_queue = None + + def __init__(self, player, job_queue): + self.player = player + self.job_queue = job_queue + + +# TODO do_skip() could get executed in another thread (it can be a job), so it looks like it can't use game.translate? +def do_skip(bot, player, job_queue=None): + game = player.game + chat = game.chat + skipped_player = game.current_player + next_player = game.current_player.next + + if skipped_player.waiting_time > 0: + skipped_player.anti_cheat += 1 + skipped_player.waiting_time -= TIME_REMOVAL_AFTER_SKIP + if (skipped_player.waiting_time < 0): + skipped_player.waiting_time = 0 + + try: + skipped_player.draw() + except DeckEmptyError: + pass + + n = skipped_player.waiting_time + send_async(bot, chat.id, + text="Waiting time to skip this player has " + "been reduced to {time} seconds.\n" + "Next player: {name}" + .format(time=n, + name=display_name(next_player.user)) + ) + logger.info("{player} was skipped!. " + .format(player=display_name(player.user))) + game.turn() + if job_queue: + start_player_countdown(bot, game, job_queue) + + else: + try: + gm.leave_game(skipped_player.user, chat) + send_async(bot, chat.id, + text="{name1} ran out of time " + "and has been removed from the game!\n" + "Next player: {name2}" + .format(name1=display_name(skipped_player.user), + name2=display_name(next_player.user))) + logger.info("{player} was skipped!. " + .format(player=display_name(player.user))) + if job_queue: + start_player_countdown(bot, game, job_queue) + + except NotEnoughPlayersError: + send_async(bot, chat.id, + text="{name} ran out of time " + "and has been removed from the game!\n" + "The game ended." + .format(name=display_name(skipped_player.user))) + + gm.end_game(chat, skipped_player.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) + player.play(card) + game = player.game + chat = game.chat + user = player.user + + us = UserSetting.get(id=user.id) + if not us: + us = UserSetting(id=user.id) + + if us.stats: + us.cards_played += 1 + + if game.choosing_color: + send_async(bot, chat.id, text=_("Please choose a color")) + + if len(player.cards) == 1: + send_async(bot, chat.id, text="UNO!") + + if len(player.cards) == 0: + send_async(bot, chat.id, + text=__("{name} won!", multi=game.translate) + .format(name=user.first_name)) + + if us.stats: + us.games_played += 1 + + if game.players_won is 0: + us.first_places += 1 + + game.players_won += 1 + + try: + gm.leave_game(user, chat) + except NotEnoughPlayersError: + send_async(bot, chat.id, + text=__("Game ended!", multi=game.translate)) + + us2 = UserSetting.get(id=game.current_player.user.id) + if us2 and us2.stats: + us2.games_played += 1 + + gm.end_game(chat, user) + + if botan: + random_int = random.randrange(1, 999999999) + botan.track(Message(random_int, user, datetime.now(), + Chat(chat.id, 'group')), + 'Played cards') + + +def do_draw(bot, player): + """Does the drawing""" + game = player.game + draw_counter_before = game.draw_counter + + try: + player.draw() + except DeckEmptyError: + send_async(bot, player.game.chat.id, + text=__("There are no more cards in the deck.", + multi=game.translate)) + + if (game.last_card.value == c.DRAW_TWO or + game.last_card.special == c.DRAW_FOUR) and \ + draw_counter_before > 0: + game.turn() + + +def do_call_bluff(bot, player): + """Handles the bluff calling""" + game = player.game + chat = game.chat + + if player.prev.bluffing: + send_async(bot, chat.id, + text=__("Bluff called! Giving 4 cards to {name}", + multi=game.translate) + .format(name=player.prev.user.first_name)) + + try: + player.prev.draw() + except DeckEmptyError: + send_async(bot, player.game.chat.id, + text=__("There are no more cards in the deck.", + multi=game.translate)) + + else: + game.draw_counter += 2 + send_async(bot, chat.id, + text=__("{name1} didn't bluff! Giving 6 cards to {name2}", + multi=game.translate) + .format(name1=player.prev.user.first_name, + name2=player.user.first_name)) + try: + player.draw() + except DeckEmptyError: + send_async(bot, player.game.chat.id, + text=__("There are no more cards in the deck.", + multi=game.translate)) + + game.turn() + + +def start_player_countdown(bot, game, job_queue): + player = game.current_player + time = player.waiting_time + + if time < MIN_FAST_TURN_TIME: + time = MIN_FAST_TURN_TIME + + if game.mode == 'fast': + if game.job: + game.job.schedule_removal() + + job = job_queue.run_once( + #lambda x,y: do_skip(bot, player), + skip_job, + time, + context=Countdown(player, job_queue) + ) + + logger.info("Started countdown for player: {player}. {time} seconds." + .format(player=display_name(player.user), time=time)) + player.game.job = job + + +def skip_job(bot, job): + player = job.context.player + game = player.game + if game_is_running(game): + job_queue = job.context.job_queue + do_skip(bot, player, job_queue) \ No newline at end of file diff --git a/bot.py b/bot.py index 43deecd..ed3f21c 100644 --- a/bot.py +++ b/bot.py @@ -19,39 +19,35 @@ import logging from datetime import datetime -from random import randint -from telegram import ParseMode, Message, Chat, InlineKeyboardMarkup, \ +from telegram import ParseMode, InlineKeyboardMarkup, \ InlineKeyboardButton from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \ CommandHandler, MessageHandler, Filters, CallbackQueryHandler from telegram.ext.dispatcher import run_async -from start_bot import start_bot -from results import (add_call_bluff, add_choose_color, add_draw, add_gameinfo, - add_no_game, add_not_started, add_other_cards, add_pass, - add_card) -from user_setting import UserSetting -from utils import display_name, get_admin_ids import card as c +import settings +import simple_commands +from actions import do_skip, do_play_card, do_draw, do_call_bluff, start_player_countdown +from gameplay_config import WAITING_TIME, DEFAULT_GAMEMODE, MIN_PLAYERS from errors import (NoGameInChatError, LobbyClosedError, AlreadyJoinedError, NotEnoughPlayersError, DeckEmptyError) -from utils import send_async, answer_async, error, TIMEOUT -from shared_vars import botan, gm, updater, dispatcher from internationalization import _, __, user_locale, game_locales -import simple_commands -import settings +from results import (add_call_bluff, add_choose_color, add_draw, add_gameinfo, + add_no_game, add_not_started, add_other_cards, add_pass, + add_card, add_mode_classic, add_mode_fast, add_mode_wild) +from shared_vars import botan, gm, updater, dispatcher +from simple_commands import help_handler +from start_bot import start_bot +from utils import display_name +from utils import send_async, answer_async, error, TIMEOUT, user_is_creator_or_admin, user_is_creator, game_is_running -from simple_commands import help - -#import json -#with open("config.json","r") as f: -# config = json.loads(f.read()) -#forbidden = config.get("black_list", None) logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.INFO) + level=logging.INFO +) logger = logging.getLogger(__name__) @user_locale @@ -76,7 +72,7 @@ def new_game(bot, update): chat_id = update.message.chat_id if update.message.chat.type == 'private': - help(bot, update) + help_handler(bot, update) else: @@ -92,6 +88,7 @@ def new_game(bot, update): game = gm.new_game(update.message.chat) game.starter = update.message.from_user game.owner.append(update.message.from_user.id) + game.mode = DEFAULT_GAMEMODE send_async(bot, chat_id, text=_("Created a new game! Join the game with /join " "and start the game with /start")) @@ -107,7 +104,7 @@ def kill_game(bot, update): games = gm.chatid_games.get(chat.id) if update.message.chat.type == 'private': - help(bot, update) + help_handler(bot, update) return if not games: @@ -117,7 +114,7 @@ def kill_game(bot, update): game = games[-1] - if user.id in game.owner or user.id in get_admin_ids(bot, chat.id): + if user_is_creator_or_admin(user, game, bot, chat): try: gm.end_game(chat, user) @@ -141,7 +138,7 @@ def join_game(bot, update): chat = update.message.chat if update.message.chat.type == 'private': - help(bot, update) + help_handler(bot, update) return try: @@ -204,11 +201,18 @@ def leave_game(bot, update): send_async(bot, chat.id, text=__("Game ended!", multi=game.translate)) else: - send_async(bot, chat.id, - text=__("Okay. Next Player: {name}", - multi=game.translate).format( - name=display_name(game.current_player.user)), - reply_to_message_id=update.message.message_id) + if game.started: + send_async(bot, chat.id, + text=__("Okay. Next Player: {name}", + multi=game.translate).format( + name=display_name(game.current_player.user)), + reply_to_message_id=update.message.message_id) + else: + send_async(bot, chat.id, + text=__("{name} left the game before it started.", + multi=game.translate).format( + name=display_name(user)), + reply_to_message_id=update.message.message_id) def select_game(bot, update): @@ -275,7 +279,7 @@ def status_update(bot, update): @game_locales @user_locale -def start_game(bot, update, args): +def start_game(bot, update, args, job_queue): """Handler for the /start command""" if update.message.chat.type != 'private': @@ -292,14 +296,17 @@ def start_game(bot, update, args): if game.started: send_async(bot, chat.id, text=_("The game has already started")) - elif len(game.players) < 2: + elif len(game.players) < MIN_PLAYERS: send_async(bot, chat.id, - text=_("At least two players must /join the game " - "before you can start it")) + text=__("At least {minplayers} players must /join the game " + "before you can start it").format(minplayers=MIN_PLAYERS)) else: - game.play_card(game.last_card) - game.started = True + # Starting a game + game.start() + + for player in game.players: + player.draw_first_hand() first_message = ( __("First player: {name}\n" @@ -321,6 +328,7 @@ def start_game(bot, update, args): timeout=TIMEOUT) send_first() + start_player_countdown(bot, game, job_queue) elif len(args) and args[0] == 'select': players = gm.userid_players[update.message.from_user.id] @@ -342,7 +350,7 @@ def start_game(bot, update, args): reply_markup=InlineKeyboardMarkup(groups)) else: - help(bot, update) + help_handler(bot, update) @user_locale @@ -472,13 +480,14 @@ def skip_player(bot, update): 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: + # You can't skip if the current player still has time left + # You can skip yourself even if you have time left (you'll still draw) + if delta < skipped_player.waiting_time and player != skipped_player: n = skipped_player.waiting_time - delta send_async(bot, chat.id, text=_("Please wait {time} second", @@ -486,47 +495,8 @@ def skip_player(bot, update): n) .format(time=n), 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 - - n = skipped_player.waiting_time - send_async(bot, chat.id, - text=__("Waiting time to skip this player has " - "been reduced to {time} second.\n" - "Next player: {name}", - "Waiting time to skip this player has " - "been reduced to {time} seconds.\n" - "Next player: {name}", - n, - multi=game.translate) - .format(time=n, - 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}", multi=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.", multi=game.translate) - .format(name=display_name(skipped_player.user))) - - gm.end_game(chat.id, skipped_player.user) + do_skip(bot, player) @game_locales @@ -540,15 +510,25 @@ def reply_to_query(bot, update): switch = None try: - user_id = update.inline_query.from_user.id + user = update.inline_query.from_user + user_id = user.id players = gm.userid_players[user_id] player = gm.userid_current[user_id] game = player.game except KeyError: add_no_game(results) else: + + # The game has not started. + # The creator may change the game mode, other users just get a "game has not started" message. if not game.started: - add_not_started(results) + if user_is_creator(user, game): + add_mode_classic(results) + add_mode_fast(results) + add_mode_wild(results) + else: + add_not_started(results) + elif user_id == game.current_player.user.id: if game.choosing_color: @@ -594,7 +574,7 @@ def reply_to_query(bot, update): @game_locales @user_locale -def process_result(bot, update): +def process_result(bot, update, job_queue): """ Handler for chosen inline results. Checks the players actions and acts accordingly. @@ -616,6 +596,13 @@ def process_result(bot, update): if result_id in ('hand', 'gameinfo', 'nogame'): return + elif result_id.startswith('mode_'): + # First 5 characters are 'mode_', the rest is the gamemode. + mode = result_id[5:] + game.set_mode(mode) + logger.info("Gamemode changed to {mode}".format(mode = mode)) + send_async(bot, chat.id, text=__("Gamemode changed to {mode}".format(mode = mode))) + return elif len(result_id) == 36: # UUID result return elif int(anti_cheat) != last_anti_cheat: @@ -637,134 +624,30 @@ def process_result(bot, update): reset_waiting_time(bot, player) do_play_card(bot, player, result_id) - if game in gm.chatid_games.get(chat.id, list()): + if game_is_running(game): send_async(bot, chat.id, text=__("Next player: {name}", multi=game.translate) .format(name=display_name(game.current_player.user))) + start_player_countdown(bot, game, job_queue) 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: - player.waiting_time = 90 + if player.waiting_time < WAITING_TIME: + player.waiting_time = WAITING_TIME send_async(bot, chat.id, - text=__("Waiting time for {name} has been reset to 90 " + text=__("Waiting time for {name} has been reset to {time} " "seconds", multi=player.game.translate) - .format(name=display_name(player.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) - player.play(card) - game = player.game - chat = game.chat - user = player.user - - us = UserSetting.get(id=user.id) - if not us: - us = UserSetting(id=user.id) - - if us.stats: - us.cards_played += 1 - - if game.choosing_color: - send_async(bot, chat.id, text=_("Please choose a color")) - - if len(player.cards) == 1: - send_async(bot, chat.id, text="UNO!") - - if len(player.cards) == 0: - send_async(bot, chat.id, - text=__("{name} won!", multi=game.translate) - .format(name=user.first_name)) - - if us.stats: - us.games_played += 1 - - if game.players_won is 0: - us.first_places += 1 - - game.players_won += 1 - - try: - gm.leave_game(user, chat) - except NotEnoughPlayersError: - send_async(bot, chat.id, - text=__("Game ended!", multi=game.translate)) - - us2 = UserSetting.get(id=game.current_player.user.id) - if us2 and us2.stats: - us2.games_played += 1 - - gm.end_game(chat, user) - - if botan: - botan.track(Message(randint(1, 1000000000), user, datetime.now(), - Chat(chat.id, 'group')), - 'Played cards') - - -def do_draw(bot, player): - """Does the drawing""" - game = player.game - draw_counter_before = game.draw_counter - - try: - player.draw() - except DeckEmptyError: - send_async(bot, player.game.chat.id, - text=__("There are no more cards in the deck.", - multi=game.translate)) - - if (game.last_card.value == c.DRAW_TWO or - game.last_card.special == c.DRAW_FOUR) and \ - draw_counter_before > 0: - game.turn() - - -def do_call_bluff(bot, player): - """Handles the bluff calling""" - game = player.game - chat = game.chat - - if player.prev.bluffing: - send_async(bot, chat.id, - text=__("Bluff called! Giving 4 cards to {name}", - multi=game.translate) - .format(name=player.prev.user.first_name)) - - try: - player.prev.draw() - except DeckEmptyError: - send_async(bot, player.game.chat.id, - text=__("There are no more cards in the deck.", - multi=game.translate)) - - else: - game.draw_counter += 2 - send_async(bot, chat.id, - text=__("{name1} didn't bluff! Giving 6 cards to {name2}", - multi=game.translate) - .format(name1=player.prev.user.first_name, - name2=player.user.first_name)) - try: - player.draw() - except DeckEmptyError: - send_async(bot, player.game.chat.id, - text=__("There are no more cards in the deck.", - multi=game.translate)) - - game.turn() + .format(name=display_name(player.user), time=WAITING_TIME)) # Add all handlers to the dispatcher and run the bot dispatcher.add_handler(InlineQueryHandler(reply_to_query)) -dispatcher.add_handler(ChosenInlineResultHandler(process_result)) +dispatcher.add_handler(ChosenInlineResultHandler(process_result, pass_job_queue=True)) dispatcher.add_handler(CallbackQueryHandler(select_game)) -dispatcher.add_handler(CommandHandler('start', start_game, pass_args=True)) +dispatcher.add_handler(CommandHandler('start', start_game, pass_args=True, pass_job_queue=True)) dispatcher.add_handler(CommandHandler('new', new_game)) dispatcher.add_handler(CommandHandler('kill', kill_game)) dispatcher.add_handler(CommandHandler('join', join_game)) diff --git a/card.py b/card.py index 20717d8..63ddd8e 100644 --- a/card.py +++ b/card.py @@ -52,6 +52,7 @@ SKIP = 'skip' VALUES = (ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, DRAW_TWO, REVERSE, SKIP) +WILD_VALUES = (ONE, TWO, THREE, FOUR, FIVE, DRAW_TWO, REVERSE, SKIP) # Special cards CHOOSE = 'colorchooser' diff --git a/database.py b/database.py index 38b5cca..0c3acfe 100644 --- a/database.py +++ b/database.py @@ -18,7 +18,7 @@ # along with this program. If not, see . -from pony.orm import Database, db_session, Optional, Required, Set, PrimaryKey +from pony.orm import Database # Database singleton db = Database() diff --git a/deck.py b/deck.py index 436a323..4f1db83 100644 --- a/deck.py +++ b/deck.py @@ -34,18 +34,7 @@ class Deck(object): self.graveyard = list() self.logger = logging.getLogger(__name__) - # Fill deck - for color in c.COLORS: - for value in c.VALUES: - self.cards.append(Card(color, value)) - if not value == c.ZERO: - self.cards.append(Card(color, value)) - - for special in c.SPECIALS * 4: - self.cards.append(Card(None, None, special=special)) - self.logger.debug(self.cards) - self.shuffle() def shuffle(self): """Shuffles the deck""" @@ -70,3 +59,28 @@ class Deck(object): def dismiss(self, card): """Returns a card to the deck""" self.graveyard.append(card) + + def _fill_classic_(self): + # Fill deck with the classic card set + self.cards.clear() + for color in c.COLORS: + for value in c.VALUES: + self.cards.append(Card(color, value)) + if not value == c.ZERO: + self.cards.append(Card(color, value)) + for special in c.SPECIALS: + for _ in range(4): + self.cards.append(Card(None, None, special=special)) + self.shuffle() + + def _fill_wild_(self): + # Fill deck with a wild card set + self.cards.clear() + for color in c.COLORS: + for value in c.WILD_VALUES: + for _ in range(4): + self.cards.append(Card(color, value)) + for special in c.SPECIALS: + for _ in range(6): + self.cards.append(Card(None, None, special=special)) + self.shuffle() diff --git a/game.py b/game.py index b2981c8..1d59d98 100644 --- a/game.py +++ b/game.py @@ -21,6 +21,8 @@ import logging import json from datetime import datetime + +from gameplay_config import DEFAULT_GAMEMODE from deck import Deck import card as c @@ -33,6 +35,8 @@ class Game(object): draw_counter = 0 players_won = 0 starter = None + mode = DEFAULT_GAMEMODE + job = None with open("config.json","r") as f: config = json.loads(f.read()) owner = config.get("admin_list", None) @@ -43,9 +47,7 @@ class Game(object): self.chat = chat self.last_card = None - while not self.last_card or self.last_card.special: - self.deck = Deck() - self.last_card = self.deck.draw() + self.deck = Deck() self.logger = logging.getLogger(__name__) @@ -64,6 +66,18 @@ class Game(object): itplayer = itplayer.next return players + def start(self): + if self.mode == None or self.mode != "wild": + self.deck._fill_classic_() + else: + self.deck._fill_wild_() + + self._first_card_() + self.started = True + + def set_mode(self, mode): + self.mode = mode + def reverse(self): """Reverses the direction of game""" self.reversed = not self.reversed @@ -76,6 +90,20 @@ class Game(object): self.current_player.turn_started = datetime.now() self.choosing_color = False + def _first_card_(self): + # In case that the player did not select a game mode + if not self.deck.cards: + self.set_mode(DEFAULT_GAMEMODE) + + # The first card should not be a special card + while not self.last_card or self.last_card.special: + self.last_card = self.deck.draw() + # If the card drawn was special, return it to the deck and loop again + if self.last_card.special: + self.deck.dismiss(self.last_card) + + self.play_card(self.last_card) + def play_card(self, card): """ Plays a card and triggers its effects. diff --git a/game_manager.py b/game_manager.py index 5c94a22..ec49bc4 100644 --- a/game_manager.py +++ b/game_manager.py @@ -79,20 +79,22 @@ class GameManager(object): for player in players: if player in game.players: raise AlreadyJoinedError() - else: - try: - self.leave_game(user, chat) - except NoGameInChatError: - pass - except NotEnoughPlayersError: - self.end_game(chat, user) - if user.id not in self.userid_players: - self.userid_players[user.id] = list() + try: + self.leave_game(user, chat) + except NoGameInChatError: + pass + except NotEnoughPlayersError: + self.end_game(chat, user) - players = self.userid_players[user.id] + if user.id not in self.userid_players: + self.userid_players[user.id] = list() + + players = self.userid_players[user.id] player = Player(game, user) + if game.started: + player.draw_first_hand() players.append(player) self.userid_current[user.id] = player @@ -113,8 +115,8 @@ class GameManager(object): p.leave() return - else: - raise NoGameInChatError + + raise NoGameInChatError game = player.game @@ -185,5 +187,4 @@ class GameManager(object): for player in players: if player.game.chat.id == chat.id: return player - else: - return None + return None diff --git a/gameplay_config.py b/gameplay_config.py new file mode 100644 index 0000000..1d2b82e --- /dev/null +++ b/gameplay_config.py @@ -0,0 +1,6 @@ +# Current gamemodes: "classic", "fast", "wild" +DEFAULT_GAMEMODE = "fast" +WAITING_TIME = 120 +TIME_REMOVAL_AFTER_SKIP = 20 +MIN_FAST_TURN_TIME = 15 +MIN_PLAYERS = 2 \ No newline at end of file diff --git a/internationalization.py b/internationalization.py index 9b09f22..784a0d6 100644 --- a/internationalization.py +++ b/internationalization.py @@ -22,7 +22,7 @@ import gettext from functools import wraps from locales import available_locales -from database import db_session +from pony.orm import db_session from user_setting import UserSetting from shared_vars import gm @@ -102,7 +102,7 @@ def user_locale(func): @wraps(func) @db_session def wrapped(bot, update, *pargs, **kwargs): - user, chat = _user_chat_from_update(update) + user = _user_chat_from_update(update)[0] with db_session: us = UserSetting.get(id=user.id) diff --git a/locales/compile.sh b/locales/compile.sh index 02046c4..0783f88 100644 --- a/locales/compile.sh +++ b/locales/compile.sh @@ -2,7 +2,7 @@ # This script compiles the unobot.po file for all languages. function compile { - cd '.\'$1'\LC_MESSAGES\' + cd './'$1'/LC_MESSAGES/' msgfmt unobot.po -o unobot.mo cd ../../ } diff --git a/locales/es_ES/LC_MESSAGES/unobot.po b/locales/es_ES/LC_MESSAGES/unobot.po index 3ab795f..e5573fa 100644 --- a/locales/es_ES/LC_MESSAGES/unobot.po +++ b/locales/es_ES/LC_MESSAGES/unobot.po @@ -173,9 +173,9 @@ msgid "The game has already started" msgstr "La partida ya ha comenzado." #: bot.py:281 -msgid "At least two players must /join the game before you can start it" +msgid "At least {minplayers} players must /join the game before you can start it" msgstr "" -"Antes de iniciar la partida, al menos dos jugadores deben unirse usando /join." +"Antes de iniciar la partida, al menos {minplayers} jugadores deben unirse usando /join." #: bot.py:297 #, python-format @@ -289,8 +289,8 @@ msgstr "Siguiente Jugador: {name}." #: bot.py:572 #, python-format -msgid "Waiting time for {name} has been reset to 90 seconds" -msgstr "El tiempo de espera para {name} se ha reiniciado a 90 segundos" +msgid "Waiting time for {name} has been reset to {time} seconds" +msgstr "El tiempo de espera para {name} se ha reiniciado a {time} segundos" #: bot.py:585 msgid "Please choose a color" @@ -415,7 +415,7 @@ msgstr "¡Estadísticas borradas y deshabilitadas! " #: settings.py:94 msgid "Set locale!" -msgstr "¡Idioma seleccionada!" +msgstr "¡Idioma seleccionado!" #: simple_commands.py msgid "" diff --git a/player.py b/player.py index 45954f1..df3ce1e 100644 --- a/player.py +++ b/player.py @@ -23,6 +23,7 @@ from datetime import datetime import card as c from errors import DeckEmptyError +from gameplay_config import WAITING_TIME class Player(object): @@ -39,15 +40,6 @@ class Player(object): self.user = user 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. if game.current_player: self.next = game.current_player @@ -63,7 +55,17 @@ class Player(object): self.drew = False self.anti_cheat = 0 self.turn_started = datetime.now() - self.waiting_time = 90 + self.waiting_time = WAITING_TIME + + def draw_first_hand(self): + try: + for _ in range(7): + self.cards.append(self.game.deck.draw()) + except DeckEmptyError: + for card in self.cards: + self.game.deck.dismiss(card) + + raise def leave(self): """Removes player from the game and closes the gap in the list""" @@ -113,7 +115,7 @@ class Player(object): _amount = self.game.draw_counter or 1 try: - for i in range(_amount): + for _ in range(_amount): self.cards.append(self.game.deck.draw()) except DeckEmptyError: diff --git a/results.py b/results.py index 1c656fd..1203154 100644 --- a/results.py +++ b/results.py @@ -94,6 +94,42 @@ def add_not_started(results): ) +def add_mode_classic(results): + """Change mode to classic""" + results.append( + InlineQueryResultArticle( + "mode_classic", + title=_("🎻 Classic mode"), + input_message_content= + InputTextMessageContent(_('Classic 🎻')) + ) + ) + + +def add_mode_fast(results): + """Change mode to classic""" + results.append( + InlineQueryResultArticle( + "mode_fast", + title=_("🚀 Sanic mode"), + input_message_content= + InputTextMessageContent(_('Gotta go fast! 🚀')) + ) + ) + + +def add_mode_wild(results): + """Change mode to classic""" + results.append( + InlineQueryResultArticle( + "mode_wild", + title=_("🐉 Wild mode"), + input_message_content= + InputTextMessageContent(_('Into the Wild~ 🐉')) + ) + ) + + def add_draw(player, results): """Add option to draw""" n = player.game.draw_counter or 1 diff --git a/shared_vars.py b/shared_vars.py index 4181aef..52f412b 100644 --- a/shared_vars.py +++ b/shared_vars.py @@ -18,12 +18,13 @@ # along with this program. If not, see . import json + +import logging from telegram.ext import Updater from telegram.contrib.botan import Botan from game_manager import GameManager from database import db -import user_setting # required to generate db mapping db.bind('sqlite', 'uno.sqlite3', create_db=True) db.generate_mapping(create_tables=True) diff --git a/simple_commands.py b/simple_commands.py index 2aad221..368513e 100644 --- a/simple_commands.py +++ b/simple_commands.py @@ -70,14 +70,27 @@ attributions = ("Attributions:\n" "Originals available on http://game-icons.net\n" "Icons edited by ɳick") +modes_explanation = ("This UNO bot has three game modes: Classic, Sanic and Wild.\n\n" + " 🎻 The Classic mode uses the conventional UNO deck and there is no auto skip.\n" + " 🚀 The Sanic mode uses the conventional UNO deck and the bot automatically skips a player if he/she takes too long to play its turn\n" + " 🐉 The Wild mode uses a deck with more special cards, less number variety and no auto skip.\n\n" + "To change the game mode, the GAME CREATOR has to type the bot nickname and a space, just like when playing a card, and all gamemode options should appear.") + @user_locale -def help(bot, update): +def help_handler(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 modes(bot, update): + """Handler for the /help command""" + send_async(bot, update.message.chat_id, text=_(modes_explanation), + parse_mode=ParseMode.HTML, disable_web_page_preview=True) + + @user_locale def source(bot, update): """Handler for the /help command""" @@ -131,7 +144,8 @@ def stats(bot, update): def register(): - dispatcher.add_handler(CommandHandler('help', help)) + dispatcher.add_handler(CommandHandler('help', help_handler)) dispatcher.add_handler(CommandHandler('source', source)) dispatcher.add_handler(CommandHandler('news', news)) dispatcher.add_handler(CommandHandler('stats', stats)) + dispatcher.add_handler(CommandHandler('modes', modes)) diff --git a/test/test_game_manager.py b/test/test_game_manager.py index 944833b..31dd070 100644 --- a/test/test_game_manager.py +++ b/test/test_game_manager.py @@ -74,7 +74,7 @@ class Test(unittest.TestCase): *(self.user1, self.chat0)) def test_leave_game(self): - g0 = self.gm.new_game(self.chat0) + self.gm.new_game(self.chat0) self.gm.join_game(self.user0, self.chat0) self.gm.join_game(self.user1, self.chat0) @@ -91,14 +91,14 @@ class Test(unittest.TestCase): *(self.user0, self.chat0)) def test_end_game(self): - g0 = self.gm.new_game(self.chat0) + 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.new_game(self.chat0) self.gm.join_game(self.user2, self.chat0) self.gm.end_game(self.chat0, self.user0) diff --git a/user_setting.py b/user_setting.py index 1141d61..c4e2c50 100644 --- a/user_setting.py +++ b/user_setting.py @@ -18,7 +18,8 @@ # along with this program. If not, see . -from database import db, Optional, Required, PrimaryKey, db_session +from pony.orm import Optional, PrimaryKey +from database import db class UserSetting(db.Entity): diff --git a/utils.py b/utils.py index 2d41bc9..37c584a 100644 --- a/utils.py +++ b/utils.py @@ -24,6 +24,7 @@ from telegram.ext.dispatcher import run_async from internationalization import _, __ from mwt import MWT +from shared_vars import gm logger = logging.getLogger(__name__) @@ -105,6 +106,22 @@ def answer_async(bot, *args, **kwargs): error(None, None, e) +def game_is_running(game): + return game in gm.chatid_games.get(game.chat.id, list()) + + +def user_is_creator(user, game): + return user.id in game.owner + + +def user_is_admin(user, bot, chat): + return user.id in get_admin_ids(bot, chat.id) + + +def user_is_creator_or_admin(user, game, bot, chat): + return user_is_creator(user, game) or user_is_admin(user, bot, chat) + + @MWT(timeout=60*60) def get_admin_ids(bot, chat_id): """Returns a list of admin IDs for a given chat. Results are cached for 1 hour."""