15 Commits

Author SHA1 Message Date
14bd90ea92 Update bot.py 2017-03-04 09:30:09 +08:00
9dde74381e Update bot.py
Removing Typos
2017-03-04 09:17:52 +08:00
683d203984 Update bot.py
Adding mode function
2017-03-04 09:17:20 +08:00
d8dfb438a3 Update game_manager.py
Changing the function end_game order to (self, user, chat)
2017-03-03 19:40:40 +08:00
52296e6df8 Update player.py 2017-02-24 14:16:52 +08:00
4c386218b0 Update player.py
Adding a new mode part in the function _card_playable.
2017-02-24 14:15:08 +08:00
393da434e8 Update game.py
Adding a new option "mode"
2017-02-24 13:39:14 +08:00
fae55f758d Merge pull request #30 from jh0ker/master
Update README.md
2017-02-24 13:32:34 +08:00
92c07d12ad Update bot.py
Changing the function process_result on result_id in c.COLORS such that there is a try-except statement to prevent a deadlock in choosing colors.
2017-02-18 00:06:41 +08:00
bffd7fb1c3 Update errors.py
Adding a new PlayerLeftError
2017-02-17 22:55:11 +08:00
69dc39bb56 Update player.py
Correcting the code once again as the last commit.
2017-02-17 22:47:16 +08:00
75e3076285 Update player.py
Reverting back to the original rule.
2017-02-17 22:05:24 +08:00
f2e7a14318 Update bot.py
Adding the function notify_me more texts in else part.
Adding the function new_game to make players create a new game and join the game with the same command.
Changing the function reset_waiting_time to 60 seconds.
Changing the function skip_player in the else part to include the stats part for skipped_player.
2017-02-17 19:35:56 +08:00
0114fe774d Update game.py
Using set()) instead of list in  self.joined_before
2017-02-17 11:45:53 +08:00
615bb35359 Combining with "patch" branch (#29)
* Update unobot.po

* Update test_player.py

* Update test_player.py

* Update test_player.py

* Update test_player.py

* Update player.py

* Update player.py

* Update internationalization.py

* Update internationalization.py

* Update test_player.py

* Update test_player.py

* Update test_player.py

* Update player.py

* Update player.py

* Update player.py

* Update test_player.py

* Update test_player.py

* revert play 4 then 4 rule

* Update test_player.py

* Update player.py

* Update player.py

* Update player.py

* Update game.py

* Update game_manager.py

* Update game_manager.py

* Update game_manager.py

* Update player.py

* Update test_player.py

* Update player.py

* Update bot.py

* Update credentials.py

* Update credentials.py

* Update bot.py

* Update game.py

* Update game_manager.py

* Update game_manager.py

* Update player.py
2017-02-16 18:43:44 +08:00
6 changed files with 351 additions and 47 deletions

110
bot.py
View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
# Copyright (c) 2016 - 2017 Jannes Höke <uno@jhoeke.de> and Karho Yau
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -23,9 +23,9 @@ from datetime import datetime
from random import randint
from telegram import ParseMode, Message, Chat, InlineKeyboardMarkup, \
InlineKeyboardButton
InlineKeyboardButton, ReplyKeyboardMarkup, Emoji
from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \
CommandHandler, MessageHandler, Filters, CallbackQueryHandler
CommandHandler, MessageHandler, Filters, CallbackQueryHandler, RegexHandler
from telegram.ext.dispatcher import run_async
from start_bot import start_bot
@ -36,7 +36,7 @@ from user_setting import UserSetting
from utils import display_name
import card as c
from errors import (NoGameInChatError, LobbyClosedError, AlreadyJoinedError,
NotEnoughPlayersError, DeckEmptyError)
NotEnoughPlayersError, DeckEmptyError, PlayerLeftError)
from utils import send_async, answer_async, error, TIMEOUT
from shared_vars import botan, gm, updater, dispatcher
from internationalization import _, __, user_locale, game_locales
@ -55,7 +55,7 @@ logger = logging.getLogger(__name__)
@user_locale
def notify_me(bot, update):
"""Handler for /notify_me command, pm people for next game"""
chat_id = update.message.chat_id
chat_id = update.message.chat.id
if update.message.chat.type == 'private':
send_async(bot,
chat_id,
@ -64,6 +64,11 @@ def notify_me(bot, update):
else:
try:
gm.remind_dict[chat_id].add(update.message.from_user.id)
send_async(bot,
chat_id,
text=_("You will be notified "
"when a new game is started in {title}.").format(
title=update.message.chat.title))
except KeyError:
gm.remind_dict[chat_id] = {update.message.from_user.id}
@ -71,7 +76,7 @@ def notify_me(bot, update):
@user_locale
def new_game(bot, update):
"""Handler for the /new command"""
chat_id = update.message.chat_id
chat_id = update.message.chat.id
if update.message.chat.type == 'private':
help(bot, update)
@ -79,19 +84,20 @@ def new_game(bot, update):
else:
if update.message.chat_id in gm.remind_dict:
for user in gm.remind_dict[update.message.chat_id]:
for user in gm.remind_dict[chat_id]:
send_async(bot,
user,
text=_("A new game has been started in {title}").format(
text=_("A new game has been started in {title}.").format(
title=update.message.chat.title))
del gm.remind_dict[update.message.chat_id]
del gm.remind_dict[chat_id]
game = gm.new_game(update.message.chat)
game.owner = update.message.from_user
send_async(bot, chat_id,
text=_("Created a new game! Join the game with /join "
text=_("Created a new game! Wait for your friends "
"and start the game with /start"))
gm.join_game(update.message.from_user, update.message.chat)
if botan:
botan.track(update.message, 'New games')
@ -162,7 +168,7 @@ def leave_game(bot, update):
reply_to_message_id=update.message.message_id)
except NotEnoughPlayersError:
gm.end_game(chat, user)
gm.end_game(user, chat)
send_async(bot, chat.id, text=__("Game ended!", multi=game.translate))
else:
@ -226,7 +232,7 @@ def status_update(bot, update):
except NoGameInChatError:
pass
except NotEnoughPlayersError:
gm.end_game(chat, user)
gm.end_game(user, chat)
send_async(bot, chat.id, text=__("Game ended!",
multi=game.translate))
else:
@ -418,7 +424,48 @@ def disable_translations(bot, update):
reply_to_message_id=update.message.message_id)
return
@game_locales
@user_locale
def mode(bot, update):
"""Handler for the /mode command"""
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 chat.type == 'private':
send_async(bot, chat.id,
text=_("Please change the group mode in the public game group with "
"the bot."))
return
if game.owner.id == user.id and not games.started:
kb = [["🎻 " + _("Original"), "🚴 " + _("Progressive UNO")]]
markup = ReplyKeyboardMarkup(kb, resize_keyboard=True, one_time_keyboard=True)
choice = send_async(bot, chat.id, text=_("Choose the game mode:"), reply_markup = markup)
if choice[0] == "🎻":
game.mode = 0
send_async(bot, chat.id, text=_("Original rules will be used."))
else if choice[0] == "🚴":
game.mode = 1
send_async(bot, chat.id, text=_("Progressive UNO rules will be used."))
return
else:
send_async(bot, chat.id,
text=_("Only the game creator ({name}) can do that when the game does not start")
.format(name=game.owner.first_name),
reply_to_message_id=update.message.message_id)
return
@game_locales
@user_locale
def skip_player(bot, update):
@ -475,7 +522,7 @@ def skip_player(bot, update):
try:
gm.leave_game(skipped_player.user, chat)
send_async(bot, chat.id,
text=__("{name1} was skipped four times in a row "
text=__("{name1} was skipped three 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),
@ -483,12 +530,17 @@ def skip_player(bot, update):
except NotEnoughPlayersError:
send_async(bot, chat.id,
text=__("{name} was skipped four times in a row "
text=__("{name} was skipped three 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)
us2 = UserSetting.get(id=skipped_player.user.id)
if us2 and us2.stats:
us2.games_played += 1
gm.end_game(skipped_player.user, chat)
@game_locales
@ -594,7 +646,13 @@ def process_result(bot, update):
elif result_id == 'pass':
game.turn()
elif result_id in c.COLORS:
game.choose_color(result_id)
try:
game.choose_color(result_id)
except PlayerLeftError:
send_async(bot, chat.id,
text=__("There are errors in choosing color. "
"Color is now unchanged.", multi=game.translate))
game.turn()
else:
reset_waiting_time(bot, player)
do_play_card(bot, player, result_id)
@ -609,10 +667,10 @@ 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 < 60:
player.waiting_time = 60
send_async(bot, chat.id,
text=__("Waiting time for {name} has been reset to 90 "
text=__("Waiting time for {name} has been reset to 60 "
"seconds", multi=player.game.translate)
.format(name=display_name(player.user)))
@ -661,7 +719,7 @@ def do_play_card(bot, player, result_id):
if us2 and us2.stats:
us2.games_played += 1
gm.end_game(chat, user)
gm.end_game(user, chat)
if botan:
botan.track(Message(randint(1, 1000000000), user, datetime.now(),
@ -694,9 +752,10 @@ def do_call_bluff(bot, player):
if player.prev.bluffing:
send_async(bot, chat.id,
text=__("Bluff called! Giving 4 cards to {name}",
text=__("Bluff called! Giving {numbers} cards to {name}",
multi=game.translate)
.format(name=player.prev.user.first_name))
.format(name=player.prev.user.first_name,
numbers=game.draw_counter))
try:
player.prev.draw()
@ -708,10 +767,11 @@ def do_call_bluff(bot, player):
else:
game.draw_counter += 2
send_async(bot, chat.id,
text=__("{name1} didn't bluff! Giving 6 cards to {name2}",
text=__("{name1} didn't bluff! Giving {numbers} cards to {name2}",
multi=game.translate)
.format(name1=player.prev.user.first_name,
name2=player.user.first_name))
name2=player.user.first_name,
numbers=game.draw_counter))
try:
player.draw()
except DeckEmptyError:
@ -738,6 +798,8 @@ dispatcher.add_handler(CommandHandler('disable_translations',
disable_translations))
dispatcher.add_handler(CommandHandler('skip', skip_player))
dispatcher.add_handler(CommandHandler('notify_me', notify_me))
dispatcher.add_handler(CommandHandler('mode', mode))
dispatcher.add_handler(RegexHandler('^(Original|Progressive UNO)$', mode, pass_groups=True))
simple_commands.register()
settings.register()
dispatcher.add_handler(MessageHandler([Filters.status_update], status_update))

View File

@ -36,3 +36,7 @@ class NotEnoughPlayersError(Exception):
class DeckEmptyError(Exception):
pass
class PlayerLeftError(Exception):
pass

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
# Copyright (c) 2016 - 2017 Jannes Höke <uno@jhoeke.de> and Karho Yau
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -36,10 +36,12 @@ class Game(object):
open = True
translate = False
players_won = 0
mode = 0
def __init__(self, chat):
self.chat = chat
self.last_card = None
self.joined_before = set()
while not self.last_card or self.last_card.special:
self.deck = Deck()

View File

@ -19,6 +19,7 @@
import logging
import random
from game import Game
from player import Player
@ -77,7 +78,8 @@ class GameManager(object):
# Don not re-add a player and remove the player from previous games in
# this chat, if he is in one of them
for player in players:
if player in game.players:
# Try to pervent someone win or leave then join again.
if player in game.players or user.id in game.joined_before:
raise AlreadyJoinedError()
else:
try:
@ -93,8 +95,14 @@ class GameManager(object):
players = self.userid_players[user.id]
player = Player(game, user)
players.append(player)
# Randomize player position.
game.joined_before.append(user.id)
if len(players) > 2:
players.insert(random.randrange(len(players)), player)
else:
players.append(player)
self.userid_current[user.id] = player
def leave_game(self, user, chat):
@ -135,7 +143,7 @@ class GameManager(object):
del self.userid_current[user.id]
del self.userid_players[user.id]
def end_game(self, chat, user):
def end_game(self, user, chat):
"""
End a game
"""

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
# Copyright (c) 2016 - 2017 Jannes Höke <uno@jhoeke.de> and Karho Yau
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -63,7 +63,7 @@ class Player(object):
self.drew = False
self.anti_cheat = 0
self.turn_started = datetime.now()
self.waiting_time = 90
self.waiting_time = 60
def leave(self):
"""Removes player from the game and closes the gap in the list"""
@ -160,25 +160,51 @@ class Player(object):
is_playable = True
last = self.game.last_card
mode = self.game.mode
self.logger.debug("Checking card " + str(card))
if (card.color != last.color and card.value != last.value and
if mode == 0: # This mode is to apply the original rule
if (card.color != last.color and card.value != last.value and
not card.special):
self.logger.debug("Card's color or value doesn't match")
is_playable = False
elif last.value == c.DRAW_TWO and not \
card.value == c.DRAW_TWO and self.game.draw_counter:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif last.special == c.DRAW_FOUR and self.game.draw_counter:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif (last.special == c.CHOOSE or last.special == c.DRAW_FOUR) and \
self.logger.debug("Card's color or value doesn't match")
is_playable = False
elif last.value == c.DRAW_TWO and self.game.draw_counter:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif last.special == c.DRAW_FOUR and self.game.draw_counter:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif (last.special == c.CHOOSE or last.special == c.DRAW_FOUR) and \
(card.special == c.CHOOSE or card.special == c.DRAW_FOUR):
self.logger.debug("Can't play colorchooser on another one")
is_playable = False
elif not last.color:
self.logger.debug("Last card has no color")
is_playable = False
self.logger.debug("Can't play colorchooser on another one")
is_playable = False
# Prevent game being locked by choosing colors.
# When player is going to leave and he doesn't select a color, it causes game lock.
elif not last.color and (last.special != c.CHOOSE and last.special != c.DRAW_FOUR):
self.logger.debug("Last card has no color")
is_playable = False
elif mode == 1: # This mode is to apply the Progressive UNO rule.
if (card.color != last.color and card.value != last.value and
not card.special):
self.logger.debug("Card's color or value doesn't match")
is_playable = False
elif last.value == c.DRAW_TWO and self.game.draw_counter and not \
card.value == c.DRAW_TWO:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif last.special == c.DRAW_FOUR and self.game.draw_counter and not \
card.special == c.DRAW_FOUR:
self.logger.debug("Player has to draw and can't counter")
is_playable = False
elif (last.special == c.CHOOSE and (card.special == c.CHOOSE or card.special == c.DRAW_FOUR)) or \
(last.special == c.DRAW_FOUR and card.special == c.CHOOSE):
self.logger.debug("Can't play colorchooser on another one")
is_playable = False
# Prevent game being locked by choosing colors.
# When player is going to leave and he doesn't select a color, it causes game lock.
elif not last.color and (last.special != c.CHOOSE and last.special != c.DRAW_FOUR):
self.logger.debug("Last card has no color")
is_playable = False
return is_playable

202
test_player.py Normal file
View File

@ -0,0 +1,202 @@
#!/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
import telegram
from game import Game
from player import Player
import card as c
import logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG)
logger = logging.getLogger(__name__)
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_two_then_four(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),
c.Card(None, None, c.DRAW_FOUR)]
expected = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.GREEN, c.DRAW_TWO), c.Card(None, None, c.DRAW_FOUR)]
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 = [c.Card(None, None, c.DRAW_FOUR)]
self.assertListEqual(p.playable_cards(), expected)
# def test_playable_cards_on_draw_four_then_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)]
# expected = [c.Card(None, None, c.DRAW_FOUR)]
# self.assertListEqual(p.playable_cards(), expected)
def test_bluffing(self):
p = Player(self.game, "Player 0")
Player(self.game, "Player 01")
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()
p.play(c.Card(None, None, c.DRAW_FOUR))
self.game.choose_color(c.GREEN)
self.assertFalse(self.game.current_player.prev.bluffing)
if __name__ == '__main__':
unittest.main()