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."""