Compare commits

..

131 commits

Author SHA1 Message Date
52b0d192ae fixed 2019-11-08 03:35:56 +00:00
dcedec6d71 translator.md TR 2019-11-07 10:45:44 +00:00
7a04c3c775 translator.py TR 2019-11-07 10:45:01 +00:00
ccfca62612 Language TR 2019-11-07 07:57:12 +00:00
3b2a9d4ec2 compile.py TR 2019-11-07 07:28:00 +00:00
bb96bbd2fe available.py TR 2019-11-07 07:26:25 +00:00
7f8c11c367
turkish 2019-11-07 15:06:42 +08:00
Jannes Höke
85b4d671c4
🔀 Merge pull request #74 from Flowiee/patch-2
Update bot.py
2019-10-13 21:32:40 +02:00
Flowiee
3af7b58d37
Update bot.py 2019-10-13 21:29:51 +02:00
Jannes Höke
6f3da7b373
🔀 Merge pull request #73 from Flowiee/patch-1
Added an useful button
2019-10-13 21:01:03 +02:00
Flowiee
67d836eaa2
Added an useful button
Added "Make your choice!" button for start game message and next player message
2019-10-13 18:24:47 +02:00
Jannes Höke
8d0f8cf887
Merge pull request #72 from pan93412/pan93412-patch-1
feature, l10n, i18n: Add pipenv support, let more strings translatable and some improves.
2019-10-07 12:28:39 +02:00
pan93412
d030c5854f
fix: `alphabetically' 2019-08-28 21:40:27 +08:00
pan93412
1936d00f2a
i18n: logger messages shouldn't be translatable.
Co-authored-by: Jim Chen <jimchen5209@gmail.com>
2019-08-28 15:44:06 +08:00
pan93412
e283d7af10 feature: add myself to contributors list 2019-08-28 15:13:23 +08:00
pan93412
b797b350cd l10n: update the zh_TW translations.
I've retranslated and update with the latest templates.

Co-authored-by: Jim Chen <jimchen5209@gmail.com>
2019-08-28 15:07:37 +08:00
pan93412
ddc9ac3d22 i18n: add pot generator and ...
- let more strings translatable.
- regenerate the pot file.
- let plural forms can be extracted properly.

Co-authored-by: Jim Chen <jimchen5209@gmail.com>
2019-08-28 15:07:20 +08:00
pan93412
9be06826c8 feature: add pipenv support. 2019-08-27 18:42:53 +08:00
Jannes Höke
77aa0d8e7b
🐛 Fix bluffing for +4s that have been played before (#67) 2019-06-10 20:53:10 +02:00
Jannes Höke
94ea61e941 Revert "new (temp) stickers for r_8 and g_9"
This reverts commit 187cc6f4ea.
2018-01-30 14:37:17 +01:00
1c713de46d Add /kick command and remove botan support (#38)
* kick handler test

* Add authentication and fix bugs

* Add help text

* minor fix

* Remove useless comments

* Get user object from reply first.

* Update bot.py

* minor fix

* update help text

* Update AUTHORS.md

* Remove old /kick implementation

* minor fix

* Manually merge upstream changes

* No need to accept args

* remove unused chat_setting.py

* botan

* import botan from submodule for compatibility with python-telegram-bot 8.1+

* fix typo

* Update requirements.txt

* delete submodule

* Drop botan support completely

* Refine the way of getting config

* Bug fix
2017-12-07 09:27:51 +01:00
Jannes Höke
187cc6f4ea new (temp) stickers for r_8 and g_9 2017-11-27 18:32:34 +01:00
Jannes Höke
5d60f8c853 renaming files 2017-11-27 18:02:55 +01:00
Jannes Höke
2ad1c253d0 add webp_lowsat 2017-11-27 18:02:29 +01:00
Jarv
ea881cf520 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.
2017-11-27 17:59:19 +01:00
Jannes Höke
9a652011db add webp 2017-11-27 17:58:41 +01:00
Jannes Höke
f2e760e321 Update requirements.txt 2017-10-23 14:02:15 +02:00
Jarv
a4c742bc00 Pull request with compile.sh and TRANSLATORS.md change (#41)
* Added a bash script to simplify the locale compilation

* Updated TRANSLATORS to include myself
2017-09-19 23:37:47 +02:00
Jarv
80fc59f124 Improved most messages in spanish (#39) 2017-09-19 17:40:14 +02:00
jimchen5209
94bfea3a09 Remove @unobot in some of the translation. (#37) 2017-09-15 02:24:28 +02:00
Jannes Höke
ed9084b5c4 install requirements 2017-08-21 21:44:30 +02:00
Jannes Höke
09d01d001b add cached admin check 2017-08-19 00:08:55 +02:00
Jannes Höke
63bf9e4035 fix import of database models, botan 2017-08-18 23:57:18 +02:00
jimchen5209
b430b26360 Add translater (#31)
* Update TRANSLATORS.md

* Update TRANSLATORS.md
2017-08-18 23:36:51 +02:00
Simon Shi
783e010956 python-telegram-bot v7 compatability + more (#35)
* Remove import of telegram.Emoji

* Using a list of filters in MessageHandler is getting deprecated

* Update requirements

Proudly upgrades to latest python-telegram-bot

* Refine readme

* Test kill command

* Another implement

* Add /kill command

* initial config support

* json config

* Add token into json

* Typo

* Add Admin list

* Refine admin & starter

* Fix an exception

* Fix typo

* refine readme

* Update help
2017-08-18 23:36:30 +02:00
Jannes Höke
8be4bc688d add missing } 2017-07-19 17:05:19 +02:00
Karho
01640d1df9 Update README.md 2017-02-24 13:26:58 +08:00
Karho
284eb91633 Merge pull request #23 from jimchen5209/patch-2
Update unobot.po
2017-02-04 13:56:46 +08:00
Karho
bd307b6195 Merge pull request #24 from maildian/master
simple fix for botan
2017-02-04 13:56:20 +08:00
Karho
82ecec2d45 Merge pull request #26 from KazamaSion/master
fix some translate mistake in zh-CN.
2017-02-04 13:54:13 +08:00
Kazama Sion
e1d559e546 fix some translate mistake in zh-CN. 2017-01-29 22:57:50 +08:00
editpes
2abdc46ad1 simple fix for botan 2016-11-22 17:51:37 +07:00
jimchen5209
3dad1dfebb Update unobot.po 2016-11-21 20:25:30 +08:00
Jannes Höke
192bbf4b27 fix name of zh_tw 2016-11-19 11:57:14 +01:00
Alexandr
c3a35e739e Fixes list number (#22) 2016-11-18 19:58:04 +01:00
Jannes Höke
58555f5f21 Fix end-of-line within string 2016-11-18 18:37:53 +01:00
Alexandr
b2ff307964 Adds ru_RU language (#21) 2016-11-18 18:29:01 +01:00
jimchen5209
35f0e9308e Zh-Tw Translatiion (#20)
I found many translation mistakes in this translation and some of the new strings weren't translated ,and I have re-translated from English.
2016-11-18 18:27:40 +01:00
Jannes Höke
09319faf94 Merge pull request #19 from editpes/master
Create requirements.txt
2016-11-08 22:50:56 +01:00
editpes
eae0a1a1b7 Update requirements.txt
change python-telegram-bot to version 5
2016-11-06 18:12:24 +07:00
editpes
22b046e961 Create requirements.txt 2016-10-30 13:00:15 +07:00
Jannes Höke
9d2524da5a replace mau_mau_bot by unobot 2016-10-27 14:44:08 +02:00
Jannes Höke
7d499b21aa Update README.md 2016-10-27 14:01:23 +02:00
Jannes Höke
b2f78fdbaa Setup instructions 2016-09-01 03:58:46 +02:00
Jannes Höke
9c74d408b7 Update README.md 2016-08-08 02:27:24 +02:00
Jannes Höke
f1b241d808 correct command handler 2016-07-05 00:40:36 +02:00
Jannes Höke
ba81ab614a use set instead of list for notify_me command 2016-07-05 00:38:02 +02:00
Jannes Höke
b0ca73a3c9 update de_DE translation 2016-07-04 22:09:01 +02:00
Jannes Höke
ede3aba641 formatting 2016-07-04 22:08:48 +02:00
Jannes Höke
d64d8c00a9 Merge pull request #14 from TiagoDanin/Translation
Update and Fix Translation
2016-07-04 22:00:02 +02:00
TiagoDanin
d78f8dcac6 Ops 2016-07-02 14:35:53 -05:00
TiagoDanin
a5eefdc1d0 Update and Fix Translation 2016-07-02 14:34:06 -05:00
Jannes Höke
8fe190133f Merge branch 'qubitnerd-qubitnerd_remind_feature' 2016-07-02 20:38:19 +02:00
Jannes Höke
b136bdf997 formatting, exception handling, documentation 2016-07-02 20:37:35 +02:00
Jannes Höke
e28bcc58ad add qubitnerd to AUTHORS 2016-07-02 20:36:29 +02:00
qubitnerd
2a710145f6 added next_game , get a pm from when next game starts 2016-07-02 21:23:34 +05:30
Jannes Höke
7f15aac773 missing arg in funciton call 2016-06-04 12:44:21 +02:00
Jannes Höke
2a52f6e36c for-variable 2016-06-04 12:43:06 +02:00
Jannes Höke
ad2ae0a752 remove player even if its not registered in player list of user (fix) 2016-06-03 08:00:13 +02:00
Jannes Höke
fe98147377 remove player even if its not registered in player list of user 2016-06-03 07:58:05 +02:00
Jannes Höke
91947abce0 remove old, empty games 2016-06-02 15:51:07 +02:00
Jannes Höke
8dcd05dbb2 kind-of bugfix for duplicate players 2016-06-02 15:36:40 +02:00
Jannes Höke
5c87e74ae2 small fixes 2016-06-02 15:03:33 +02:00
Jannes Höke
beaa46f8e4 translation issues 2016-05-27 14:03:12 +02:00
Jannes Höke
b8e8a7e5de improve unit test of bluffing 2016-05-27 14:02:22 +02:00
Jannes Höke
569850fade get game before ending it 2016-05-27 11:31:06 +02:00
Jannes Höke
87251eeb2e add zh_CN to available locales 2016-05-25 20:18:40 +02:00
Jannes Höke
0f72532825 Merge branch 'imlonghao-zh_CN-patch' 2016-05-25 16:58:30 +02:00
Jannes Höke
cafa398ec8 Merge branch 'zh_CN-patch' of https://github.com/imlonghao/unocn_bot into imlonghao-zh_CN-patch
Conflicts:
	TRANSLATORS.md
2016-05-25 16:58:04 +02:00
imlonghao
cc88876c69
make zh_CN compatible with plurals 2016-05-25 22:44:51 +08:00
Jannes Höke
d8d31604e3 update TRANSLATORS.md 2016-05-25 05:56:18 +02:00
Jannes Höke
e53c45ed2f lang empty by default 2016-05-25 05:54:12 +02:00
Jannes Höke
857c17b3d4 change lang default en -> en_US 2016-05-25 05:35:30 +02:00
Jannes Höke
fea63f764a ultima carta 2016-05-25 04:36:47 +02:00
Jannes Höke
9e98de41c8 add plurals to es_ES (incomplete) 2016-05-25 04:25:10 +02:00
Jannes Höke
9f88f7ff32 make zh_TW compatible with plurals 2016-05-25 04:15:04 +02:00
Jannes Höke
6877063a2c make zh_HK compatible with plurals 2016-05-25 04:08:35 +02:00
Jannes Höke
fb86f6df05 add plurals to it_IT (incomplete) 2016-05-25 03:59:22 +02:00
Jannes Höke
5a71cbaae0 singular first place 2016-05-25 03:46:25 +02:00
Jannes Höke
f43cfc2190 fix english plurales 2016-05-25 03:14:07 +02:00
Jannes Höke
7672e1b47a remove logging call 2016-05-25 03:01:37 +02:00
Jannes Höke
7c250c6f79 don't record played cards if stats not enabled 2016-05-25 03:01:25 +02:00
Jannes Höke
bcaea68ef9 Merge pull request #12 from TiagoDanin/Update-pt_BR
Update pt_BR (Add plurals)
2016-05-24 17:10:06 +02:00
TiagoDanin
3ba225e7a5
Update pt_BR (Add plurals) 2016-05-24 10:02:17 -05:00
Jannes Höke
6901e7f4d0 implement plurals, update de_DE translation to use plurals 2016-05-24 15:49:23 +02:00
Jannes Höke
c54ffd83e5 update translator names and links 2016-05-24 11:25:20 +02:00
Jannes Höke
2041e3cb1f add icon attributions to source command 2016-05-24 09:58:04 +02:00
Jannes Höke
72515e37ed fix games first place stats 2016-05-24 09:58:04 +02:00
Jannes Höke
d5cac440ec use icons for draw and pass 2016-05-24 09:58:04 +02:00
Jannes Höke
23adfe2cb6 Merge pull request #11 from TiagoDanin/master
Small update in translations
2016-05-24 09:46:07 +02:00
TiagoDanin
de177e1137
Small update in translations 2016-05-23 12:57:41 -05:00
imlonghao
962f550f96
Add me to the Translators.md 2016-05-23 20:11:26 +08:00
imlonghao
06ae098a5f
add locale: zh_CN 2016-05-23 20:08:02 +08:00
Jannes Höke
4a51c7bfb6 change loglevel to INFO 2016-05-23 13:30:57 +02:00
Jannes Höke
7fd3c662a5 fix issue where people would join a game twice 2016-05-23 13:14:42 +02:00
Jannes Höke
ecc0b3adc7 add id_ID locale 2016-05-23 13:08:43 +02:00
Jannes Höke
e7e3fbf4ce add locales: es_ES, zh_HK, zh_TW 2016-05-23 12:20:31 +02:00
Jannes Höke
0c3f623cd9 small fixes & translation updates 2016-05-23 01:54:56 +02:00
Jannes Höke
0e680c6e30 update it_IT and add pt_BR 2016-05-23 00:45:44 +02:00
Jannes Höke
c7b6649438 add italian translations (big thanks to Carola and nick!) 2016-05-22 23:35:16 +02:00
Jannes Höke
c477701b75 Merge pull request #8 from jh0ker/fix-code
Fix code
2016-05-22 19:24:59 +02:00
Jannes Höke
ba47f4c19e final version? 2016-05-22 19:21:51 +02:00
Jannes Höke
4cdffffa5f Optional multi-translations 2016-05-22 17:02:27 +02:00
Jannes Höke
005445c4dd add database file to gitignore 2016-05-22 14:47:26 +02:00
Jannes Höke
6c610c1aeb settings UI added, save locale to database 2016-05-22 14:45:51 +02:00
Jannes Höke
cddf13dc5d add __ function to translate complete stack, add dummy decorators to pull locales from db 2016-05-22 03:13:05 +02:00
Jannes Höke
5ece46527a locales are working, added de_DE locale 2016-05-21 21:41:38 +02:00
Jannes Höke
becc7e28dc fix broken method call 2016-05-21 18:56:58 +02:00
Jannes Höke
a02813477a more stable first-card drawing 2016-05-21 18:56:27 +02:00
Jannes Höke
73365f49fc draw method reset draw counter on empty deck 2016-05-21 18:55:53 +02:00
Jannes Höke
d5b76c5c12 fix result list generation 2016-05-21 18:55:03 +02:00
Jannes Höke
a39fa85b3b more translation supprt 2016-05-20 18:35:21 +02:00
Jannes Höke
aee310ec9c handle empty decks on player join 2016-05-20 18:34:27 +02:00
Jannes Höke
0dcd1f6cdc use regular formatting 2016-05-20 17:55:08 +02:00
Jannes Höke
2316ab8a1c pot formatting 2016-05-20 17:53:01 +02:00
Jannes Höke
8af8852d05 initial translation support 2016-05-19 23:18:05 +02:00
Jannes Höke
204b057810 add encoding 2016-05-19 23:15:46 +02:00
Jannes Höke
9936d97373 update test_end_game 2016-05-19 21:29:07 +02:00
Jannes Höke
424219d825 fix end_game 2016-05-19 21:28:04 +02:00
Jannes Höke
c9f7c09a46 update readme 2016-05-19 20:57:32 +02:00
Jannes Höke
6204868a18 separate game logic from bot interface,
introduce exceptions instead of boolean returns,
remove repetitive code,
begin unit tests,
improve docstrings,
update to python-telegram-bot==4.1.1,
add ponyorm settings classes (unused)
2016-05-19 20:56:52 +02:00
162 changed files with 7554 additions and 1083 deletions

8
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Config file
config.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -47,7 +50,7 @@ coverage.xml
# Translations
*.mo
*.pot
# *.pot
# Django stuff:
*.log
@ -63,3 +66,6 @@ target/
# PyCharm
.idea
# Database file
uno.sqlite3

View file

@ -7,5 +7,8 @@
The following wonderful people contributed directly or indirectly to this project:
- [imlonghao](https://github.com/imlonghao)
- [pan93412](https://github.com/pan93412)
- [qubitnerd](https://github.com/qubitnerd)
- [SYHGroup](https://github.com/SYHGroup)
Please add yourself here alphabetically when you submit your first pull request.

332
ISMCTS.py
View file

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

13
Pipfile Normal file
View file

@ -0,0 +1,13 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
python-telegram-bot = "==8.1.1"
pony = "*"
[requires]
python_version = "3.7"

49
Pipfile.lock generated Normal file
View file

@ -0,0 +1,49 @@
{
"_meta": {
"hash": {
"sha256": "de56c4d5f516205e99d141cd7d372f67b602b6f981306971c01ffe25a5abf5c6"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
],
"version": "==2019.6.16"
},
"future": {
"hashes": [
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
],
"version": "==0.17.1"
},
"pony": {
"hashes": [
"sha256:55bb9d4d12029d8c2bbbc7a284970e72225035db7e6370c0a15ec93d1886fe88"
],
"index": "pypi",
"version": "==0.7.10"
},
"python-telegram-bot": {
"hashes": [
"sha256:238c4a88b09d93c52d413bcf7e7fe14dfeb02f5f9222ffe4cafd4bd3d55489a3",
"sha256:997983e5082dc6aa811bce3a6014731201fc64b0a9c02fdb26beac686029d94b"
],
"index": "pypi",
"version": "==8.1.1"
}
},
"develop": {}
}

View file

@ -1,10 +1,24 @@
# UNO Bot
Telegram Bot that allows you to play the popular card game UNO via inline queries. The bot currently runs as [@mau_mau_bot](http://telegram.me/mau_mau_bot)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](./LICENSE)
Telegram Bot that allows you to play the popular card game UNO via inline queries. The bot currently runs as [@unobot](http://telegram.me/unobot).
To run the bot yourself, you will need:
- 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
- Python (tested with 3.4+)
- The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module
- [Pony ORM](https://ponyorm.com/)
Get a bot token from [@BotFather](http://telegram.me/BotFather), place it in `credentials.py` and run the bot with `python3 bot.py`
## Setup
- Get a bot token from [@BotFather](http://telegram.me/BotFather) and change configurations in `config.json`.
- Convert all language files from `.po` files to `.mo` by executing the bash script `compile.sh` located in the `locales` folder.
Another option is: `find . -maxdepth 2 -type d -name 'LC_MESSAGES' -exec bash -c 'msgfmt {}/unobot.po -o {}/unobot.mo' \;`.
- Use `/setinline` and `/setinlinefeedback` with BotFather for your bot.
- Install requirements (using a `virtualenv` is recommended): `pip install -r requirements.txt`
Code documentation is minimal but there
You can change some gameplay parameters like turn times, minimum amount of players and default gamemode in `config.json`.
Current gamemodes available: classic, fast and wild. Check the details with the `/modes` command.
Then run the bot with `python3 bot.py`.
Code documentation is minimal but there.

18
TRANSLATORS.md Normal file
View file

@ -0,0 +1,18 @@
# Translators
The following awesome people contributed to this project by translating it:
| Locale | Translators |
|--------|--------------------------------------------------------------------------------------------------------------|
| de_DE | [Jannes Höke](https://github.com/jh0ker) |
| es_ES | [José.A Rojo](https://github.com/J4RV), [Ricardo Valverde Hernández](https://telegram.me/rivh1), Victor, Yuga|
| id_ID | [Erwin Guo](https://www.facebook.com/erwinfransiscus) |
| it_IT | Carola Mariano, ɳick |
| pt_BR | [João Rodrigo Couto de Oliveira](http://twitter.com/JoaoRodrigoJR)
| tr_TR | [Kasım Ağca](https://telegram.me/Holytotem)
|
| zh_CN | [imlonghao](https://github.com/imlonghao) |
| zh_HK | [Jed Cheng](https://www.facebook.com/profile.php?id=100002258388821) |
| zh_TW | [Eugene Lam](https://www.facebook.com/eugenelam1118), [jimchen5209](https://www.youtube.com/user/jimchen5209), [pan93412](https://www.github.com/pan93412)|
Please add yourself here alphabetically when you submit your first translation.

213
actions.py Normal file
View file

@ -0,0 +1,213 @@
import random
import logging
import card as c
from datetime import datetime
from telegram import Message, Chat
from config import TIME_REMOVAL_AFTER_SKIP, MIN_FAST_TURN_TIME
from errors import DeckEmptyError, NotEnoughPlayersError
from internationalization import __, _
from shared_vars import gm
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}", multi=game.translate)
.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}", multi=game.translate)
.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.", multi=game.translate)
.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", multi=game.translate))
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)
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)

950
bot.py

File diff suppressed because it is too large Load diff

42
card.py
View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -17,8 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram.emoji import Emoji
# Colors
RED = 'r'
BLUE = 'b'
@ -29,10 +28,10 @@ BLACK = 'x'
COLORS = (RED, BLUE, GREEN, YELLOW)
COLOR_ICONS = {
RED: Emoji.HEAVY_BLACK_HEART,
BLUE: Emoji.BLUE_HEART,
GREEN: Emoji.GREEN_HEART,
YELLOW: Emoji.YELLOW_HEART,
RED: '❤️',
BLUE: '💙',
GREEN: '💚',
YELLOW: '💛',
BLACK: '⬛️'
}
@ -53,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'
@ -114,17 +114,9 @@ STICKERS = {
'y_skip': 'BQADBAADQwIAAl9XmQABO_AZKtxY6IMC',
'y_reverse': 'BQADBAADQQIAAl9XmQABZdQFahGG6UQC',
'draw_four': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_r': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_b': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_g': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'draw_four_y': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'colorchooser': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_r': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_b': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_g': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'colorchooser_y': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'option_draw': 'BQADBAADzAIAAl9XmQABTkPaOqA5HIMC',
'option_pass': 'BQADBAADzgIAAl9XmQABWSDq3RIg3c0C',
'option_draw': 'BQADBAAD-AIAAl9XmQABxEjEcFM-VHIC',
'option_pass': 'BQADBAAD-gIAAl9XmQABcEkAAbaZ4SicAg',
'option_bluff': 'BQADBAADygIAAl9XmQABJoLfB9ntI2UC',
'option_info': 'BQADBAADxAIAAl9XmQABC5v3Z77VLfEC'
}
@ -188,9 +180,7 @@ STICKERS_GREY = {
class Card(object):
"""
This class represents a card.
"""
"""This class represents an UNO card"""
def __init__(self, color, value, special=None):
self.color = color
@ -199,9 +189,6 @@ class Card(object):
def __str__(self):
if self.special:
if self.color:
return '%s_%s' % (self.special, self.color)
else:
return self.special
else:
return '%s_%s' % (self.color, self.value)
@ -217,14 +204,7 @@ class Card(object):
def __eq__(self, other):
"""Needed for sorting the cards"""
s1 = str(self)
s2 = str(other)
return (s1 == s2
if not self.special else
s1 == s2 or
s1[:-2] == s2[:-2] or
s1[:-2] == s2 or
s1 == s2[:-2])
return str(self) == str(other)
def __lt__(self, other):
"""Needed for sorting the cards"""
@ -232,7 +212,7 @@ class Card(object):
def from_str(string):
""" Decode a Card object from a string """
"""Decodes a Card object from a string"""
if string not in SPECIALS:
color, value = string.split('_')
return Card(color, value)

12
config.json.example Normal file
View file

@ -0,0 +1,12 @@
{
"token": "token_here",
"admin_list": [0],
"open_lobby": true,
"enable_translations": false,
"workers": 32,
"default_gamemode": "fast",
"waiting_time": 120,
"time_removal_after_skip": 20,
"min_fast_turn_time": 15,
"min_players": 2
}

35
config.py Normal file
View file

@ -0,0 +1,35 @@
#!/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 json
with open("config.json","r") as f:
config = json.loads(f.read())
TOKEN=config.get("token")
WORKERS=config.get("workers", 32)
ADMIN_LIST = config.get("admin_list", None)
OPEN_LOBBY = config.get("open_lobby", True)
ENABLE_TRANSLATIONS = config.get("enable_translations", False)
DEFAULT_GAMEMODE = config.get("default_gamemode", "fast")
WAITING_TIME = config.get("waiting_time", 120)
TIME_REMOVAL_AFTER_SKIP = config.get("time_removal_after_skip", 20)
MIN_FAST_TURN_TIME = config.get("min_fast_turn_time", 15)
MIN_PLAYERS = config.get("min_players", 2)

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -17,5 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
TOKEN = 'TOKEN'
BOTAN_TOKEN = '' # Optional: Add a botan.io token if you want bot statistics
from pony.orm import Database
# Database singleton
db = Database()

76
deck.py
View file

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

61
game.py
View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -18,36 +19,38 @@
import logging
from config import ADMIN_LIST, OPEN_LOBBY, DEFAULT_GAMEMODE, ENABLE_TRANSLATIONS
from datetime import datetime
from deck import Deck
import card as c
class Game(object):
""" This class represents a game of UNO """
current_player = None
reversed = False
draw_counter = 0
choosing_color = False
started = False
owner = None
open = True
draw_counter = 0
players_won = 0
starter = None
mode = DEFAULT_GAMEMODE
job = None
owner = ADMIN_LIST
open = OPEN_LOBBY
translate = ENABLE_TRANSLATIONS
def __init__(self, chat):
self.chat = chat
self.deck = Deck()
self.last_card = self.deck.draw()
self.last_card = None
while self.last_card.special:
self.deck.cards.append(self.last_card)
self.deck.shuffle()
self.last_card = self.deck.draw()
self.deck = Deck()
self.logger = logging.getLogger(__name__)
@property
def players(self):
"""Returns a list of all players in this game"""
players = list()
if not self.current_player:
return players
@ -60,19 +63,50 @@ 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):
""" Reverse the direction of play """
"""Reverses the direction of game"""
self.reversed = not self.reversed
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.current_player = self.current_player.next
self.current_player.drew = False
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):
""" 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.last_card = card
@ -103,4 +137,3 @@ class Game(object):
"""Carries out the color choosing and turns the game"""
self.last_card.color = color
self.turn()
self.choosing_color = False

View file

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -21,6 +22,8 @@ import logging
from game import Game
from player import Player
from errors import (AlreadyJoinedError, LobbyClosedError, NoGameInChatError,
NotEnoughPlayersError)
class GameManager(object):
@ -30,6 +33,8 @@ class GameManager(object):
self.chatid_games = dict()
self.userid_players = dict()
self.userid_current = dict()
self.remind_dict = dict()
self.logger = logging.getLogger(__name__)
def new_game(self, chat):
@ -38,22 +43,31 @@ class GameManager(object):
"""
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)
if chat_id not in self.chatid_games:
self.chatid_games[chat_id] = list()
# remove old games
for g in list(self.chatid_games[chat_id]):
if not g.players:
self.chatid_games[chat_id].remove(g)
self.chatid_games[chat_id].append(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 """
self.logger.info("Joining game with id " + str(chat_id))
self.logger.info("Joining game with id " + str(chat.id))
try:
game = self.chatid_games[chat_id][-1]
game = self.chatid_games[chat.id][-1]
except (KeyError, IndexError):
return None
raise NoGameInChatError()
if not game.open:
raise LobbyClosedError()
if user.id not in self.userid_players:
self.userid_players[user.id] = list()
@ -61,28 +75,54 @@ class GameManager(object):
players = self.userid_players[user.id]
# 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:
if player in game.players:
return False
else:
self.leave_game(user, chat_id)
raise AlreadyJoinedError()
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()
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
return True
def leave_game(self, user, chat_id):
def leave_game(self, user, chat):
""" Remove a player from its current game """
try:
players = self.userid_players[user.id]
games = self.chatid_games[chat_id]
for player in players:
for game in games:
if player in game.players:
player = self.player_for_user_in_chat(user, chat)
players = self.userid_players.get(user.id, list())
if not player:
games = self.chatid_games[chat.id]
for g in games:
for p in g.players:
if p.user.id == user.id:
if p is g.current_player:
g.turn()
p.leave()
return
raise NoGameInChatError
game = player.game
if len(game.players) < 3:
raise NotEnoughPlayersError()
if player is game.current_player:
game.turn()
@ -90,49 +130,61 @@ class GameManager(object):
players.remove(player)
# If this is the selected game, switch to another
if self.userid_current[user.id] is player:
if len(players):
if self.userid_current.get(user.id, None) is player:
if players:
self.userid_current[user.id] = players[0]
else:
del self.userid_current[user.id]
return True
else:
return False
del self.userid_players[user.id]
except KeyError:
return False
def end_game(self, chat_id, user):
def end_game(self, chat, user):
"""
End a game
"""
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
self.logger.info("Game in chat " + str(chat.id) + " ended")
# Find the correct game instance to end
player = self.player_for_user_in_chat(user, chat)
if not player:
raise NoGameInChatError
game = player.game
# Clear game
for player_in_game in game.players:
this_users_players = \
self.userid_players.get(player_in_game.user.id, list())
try:
this_users_players.remove(player_in_game)
except ValueError:
pass
if this_users_players:
try:
self.userid_current[player_in_game.user.id] = this_users_players[0]
except KeyError:
pass
else:
try:
del self.userid_players[player_in_game.user.id]
except KeyError:
pass
try:
del self.userid_current[player_in_game.user.id]
except KeyError:
pass
self.chatid_games[chat.id].remove(game)
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:
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 player.ai:
continue
this_users_players = self.userid_players[player.user.id]
this_users_players.remove(player)
if len(this_users_players) is 0:
del self.userid_players[player.user.id]
del self.userid_current[player.user.id]
else:
self.userid_current[player.user.id] = this_users_players[0]
self.chatid_games[chat_id].remove(the_game)
return
if player.game.chat.id == chat.id:
return player
return None

12
genpot.sh Normal file
View file

@ -0,0 +1,12 @@
#!/usr/bin/bash
currentVer='1.0'
xgettext *.py -o ./locales/unobot.pot --foreign-user \
--package-name="uno_bot" \
--package-version="$currentVer" \
--msgid-bugs-address='uno@jhoeke.de' \
--keyword=__ \
--keyword=_ \
--keyword=_:1,2 \
--keyword=__:1,2

BIN
images/png/option_draw2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
images/png/option_pass2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
images/webp/b_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

BIN
images/webp/b_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
images/webp/b_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
images/webp/b_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/webp/b_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
images/webp/b_5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
images/webp/b_6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
images/webp/b_7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
images/webp/b_8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
images/webp/b_9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
images/webp/b_draw.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
images/webp/b_reverse.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
images/webp/b_skip.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/webp/draw_four.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
images/webp/g_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
images/webp/g_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
images/webp/g_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
images/webp/g_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
images/webp/g_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
images/webp/g_5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
images/webp/g_6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
images/webp/g_7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
images/webp/g_8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
images/webp/g_9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
images/webp/g_draw.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/webp/g_reverse.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
images/webp/g_skip.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
images/webp/r_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

BIN
images/webp/r_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
images/webp/r_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

BIN
images/webp/r_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
images/webp/r_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
images/webp/r_5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
images/webp/r_6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
images/webp/r_7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
images/webp/r_8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
images/webp/r_9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
images/webp/r_draw.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

BIN
images/webp/r_reverse.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
images/webp/r_skip.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
images/webp/y_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
images/webp/y_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
images/webp/y_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
images/webp/y_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
images/webp/y_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
images/webp/y_5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/webp/y_6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

BIN
images/webp/y_7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
images/webp/y_8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

BIN
images/webp/y_9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
images/webp/y_draw.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/webp/y_reverse.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
images/webp/y_skip.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
images/webp_lowsat/b_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
images/webp_lowsat/b_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
images/webp_lowsat/b_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
images/webp_lowsat/b_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
images/webp_lowsat/b_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
images/webp_lowsat/b_5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
images/webp_lowsat/b_6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/webp_lowsat/b_7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
images/webp_lowsat/b_8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
images/webp_lowsat/b_9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
images/webp_lowsat/g_0.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
images/webp_lowsat/g_1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
images/webp_lowsat/g_2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
images/webp_lowsat/g_3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
images/webp_lowsat/g_4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show more