106 Commits

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

11
.gitignore vendored
View File

@ -1,3 +1,9 @@
# macOS
.DS_Store
# Pyenv
.python-version
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -47,7 +53,7 @@ coverage.xml
# Translations
*.mo
*.pot
# *.pot
# Django stuff:
*.log
@ -63,3 +69,6 @@ target/
# PyCharm
.idea
# Database file
uno.sqlite3

View File

@ -7,5 +7,6 @@
The following wonderful people contributed directly or indirectly to this project:
- [imlonghao](https://github.com/imlonghao)
- [qubitnerd](https://github.com/qubitnerd)
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()

View File

@ -1,10 +1,20 @@
# 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)
# UNO Bot (zh_TW)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](./LICENSE)
forked from [jh0ker/mau_mau_bot](https://github.com/jh0ker/mau_mau_bot)
Telegram Bot that allows you to play the popular card game UNO via inline queries.
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
- The [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) module version 5.0.0
- [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 place it in `credentials.py`
- Use `/setinline` and `/setinlinefeedback` with BotFather for your bot
Then run the bot with `python3 bot.py`
Code documentation is minimal but there

16
TRANSLATORS.md Normal file
View File

@ -0,0 +1,16 @@
# 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 | [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) |
| 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) |
Please add yourself here alphabetically when you submit your first translation.

910
bot.py

File diff suppressed because it is too large Load Diff

37
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>
@ -114,17 +115,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 +181,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,10 +190,7 @@ class Card(object):
def __str__(self):
if self.special:
if self.color:
return '%s_%s' % (self.special, self.color)
else:
return self.special
return self.special
else:
return '%s_%s' % (self.color, self.value)
@ -216,23 +204,16 @@ class Card(object):
return '%s%s' % (COLOR_ICONS[self.color], self.value.capitalize())
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])
"""Needed for sorting the cards"""
return str(self) == str(other)
def __lt__(self, other):
""" Needed for sorting the cards """
"""Needed for sorting the cards"""
return str(self) < str(other)
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)

21
chat_setting.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
pass

24
database.py Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pony.orm import Database, db_session, Optional, Required, Set, PrimaryKey
# Database singleton
db = Database()

26
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):
@ -45,26 +48,25 @@ class Deck(object):
self.shuffle()
def shuffle(self):
""" Shuffle the deck """
"""Shuffles the deck"""
self.logger.debug("Shuffling Deck")
shuffle(self.cards)
def draw(self):
""" Draw a card from this deck """
"""Draws 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())
self.shuffle()
return self.draw()
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):
""" All played cards should be returned into the deck """
# if card.special:
# card.color = None
"""Returns a card to the deck"""
self.graveyard.append(card)

38
errors.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class NoGameInChatError(Exception):
pass
class AlreadyJoinedError(Exception):
pass
class LobbyClosedError(Exception):
pass
class NotEnoughPlayersError(Exception):
pass
class DeckEmptyError(Exception):
pass

26
game.py
View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -33,21 +34,22 @@ class Game(object):
started = False
owner = None
open = True
translate = False
players_won = 0
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()
while not self.last_card or self.last_card.special:
self.deck = Deck()
self.last_card = self.deck.draw()
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
@ -61,18 +63,23 @@ class Game(object):
return players
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 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
@ -100,7 +107,6 @@ class Game(object):
self.choosing_color = True
def choose_color(self, color):
""" Carries out the color choosing and turns the game """
"""Carries out the color choosing and turns the game"""
self.last_card.color = color
self.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,78 +75,115 @@ 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
raise AlreadyJoinedError()
else:
self.leave_game(user, chat_id)
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)
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:
if player is game.current_player:
game.turn()
player = self.player_for_user_in_chat(user, chat)
players = self.userid_players.get(user.id, list())
player.leave()
players.remove(player)
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()
# If this is the selected game, switch to another
if self.userid_current[user.id] is player:
if len(players):
self.userid_current[user.id] = players[0]
else:
del self.userid_current[user.id]
return True
p.leave()
return
else:
return False
raise NoGameInChatError
except KeyError:
return False
game = player.game
def end_game(self, chat_id, user):
if len(game.players) < 3:
raise NotEnoughPlayersError()
if player is game.current_player:
game.turn()
player.leave()
players.remove(player)
# If this is the selected game, switch to another
if self.userid_current.get(user.id, None) is player:
if players:
self.userid_current[user.id] = players[0]
else:
del self.userid_current[user.id]
del self.userid_players[user.id]
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
for player in players:
for game in games:
if player in game.players:
the_game = game
break
if the_game:
break
else:
return
player = self.player_for_user_in_chat(user, chat)
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]
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:
self.userid_current[player.user.id] = this_users_players[0]
try:
del self.userid_players[player_in_game.user.id]
except KeyError:
pass
self.chatid_games[chat_id].remove(the_game)
return
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:
if player.game.chat.id == chat.id:
return player
else:
return None

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

171
internationalization.py Normal file
View File

@ -0,0 +1,171 @@
#!/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 gettext
from functools import wraps
from locales import available_locales
from database import db_session
from user_setting import UserSetting
from shared_vars import gm
GETTEXT_DOMAIN = 'unobot'
GETTEXT_DIR = 'locales'
class _Underscore(object):
"""Class to emulate flufl.i18n behaviour, but with plural support"""
def __init__(self):
self.translators = {
locale: gettext.GNUTranslations(
open(gettext.find(
GETTEXT_DOMAIN, GETTEXT_DIR, languages=[locale]
), 'rb')
)
for locale
in available_locales.keys()
if locale != 'zh_TW' # No translation file for zh_TW
}
self.locale_stack = list()
def push(self, locale):
self.locale_stack.append(locale)
def pop(self):
if self.locale_stack:
return self.locale_stack.pop()
else:
return None
@property
def code(self):
if self.locale_stack:
return self.locale_stack[-1]
else:
return None
def __call__(self, singular, plural=None, n=1, locale=None):
if not locale:
locale = self.locale_stack[-1]
if locale not in self.translators.keys():
if n is 1:
return singular
else:
return plural
translator = self.translators[locale]
if plural is None:
return translator.gettext(singular)
else:
return translator.ngettext(singular, plural, n)
_ = _Underscore()
def __(singular, plural=None, n=1, multi=False):
"""Translates text into all locales on the stack"""
translations = list()
if not multi and len(set(_.locale_stack)) >= 1:
translations.append(_(singular, plural, n, 'zh_TW'))
else:
for locale in _.locale_stack:
translation = _(singular, plural, n, locale)
if translation not in translations:
translations.append(translation)
return '\n'.join(translations)
def user_locale(func):
@wraps(func)
@db_session
def wrapped(bot, update, *pargs, **kwargs):
user, chat = _user_chat_from_update(update)
with db_session:
us = UserSetting.get(id=user.id)
if us and us.lang != 'en':
_.push(us.lang)
else:
_.push('zh_TW')
result = func(bot, update, *pargs, **kwargs)
_.pop()
return result
return wrapped
def game_locales(func):
@wraps(func)
@db_session
def wrapped(bot, update, *pargs, **kwargs):
user, chat = _user_chat_from_update(update)
player = gm.player_for_user_in_chat(user, chat)
locales = list()
if player:
for player in player.game.players:
us = UserSetting.get(id=player.user.id)
if us and us.lang != 'en':
loc = us.lang
else:
loc = 'zh_TW'
if loc in locales:
continue
_.push(loc)
locales.append(loc)
result = func(bot, update, *pargs, **kwargs)
while _.code:
_.pop()
return result
return wrapped
def _user_chat_from_update(update):
try:
user = update.message.from_user
chat = update.message.chat
except (NameError, AttributeError):
try:
user = update.inline_query.from_user
chat = gm.userid_current[user.id].game.chat
except KeyError:
chat = None
except (NameError, AttributeError):
try:
user = update.chosen_inline_result.from_user
chat = gm.userid_current[user.id].game.chat
except (NameError, AttributeError, KeyError):
chat = None
return user, chat

21
locales/__init__.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .available import available_locales

30
locales/available.py Normal file
View File

@ -0,0 +1,30 @@
#!/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/>.
"""Defines a dict of all available locales to the language name"""
OFFSET = 127462 - ord('A')
def flag(code):
return chr(ord(code[0]) + OFFSET) + chr(ord(code[1]) + OFFSET)
available_locales = {'zh_TW': flag('TW') + ' 中文(台灣)'}

407
locales/unobot.pot Normal file
View File

@ -0,0 +1,407 @@
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#: bot.py:224
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: uno_bot 0.1\n"
"Report-Msgid-Bugs-To: uno@jhoeke.de\n"
"POT-Creation-Date: 2016-05-19 22:38+0200\n"
"PO-Revision-Date: 2016-05-19 22:38+0200\n"
"Last-Translator: Jannes Höke <uno@jhoeke.de>\n"
"Language-Team: en <uno@jhoeke.de>\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: bot.py:60
#, fuzzy
msgid "Follow these steps:\n"
"\n"
"1. Add this bot to a group\n"
"2. In the group, start a new game with /new or join an already running game "
"with /join\n"
"3. After at least two players have joined, start the game with /start\n"
"4. Type <code>@unobot</code> into your chat box and hit <b>space</b>, "
"or click the <code>via @unobot</code> text next to messages. You will "
"see your cards (some greyed out), any extra options like drawing, and a <b>?"
"</b> to see the current game state. The <b>greyed out cards</b> are those "
"you <b>can not play</b> at the moment. Tap an option to execute the selected "
"action.\n"
"Players can join the game at any time. To leave a game, use /leave. If a "
"player takes more than 90 seconds to play, you can use /skip to skip that "
"player. "
"Use /notify_me to receive a private message when a new game is started.\n"
"\n"
"<b>Language</b> and other settings: /settings\n"
"Other commands (only game creator):\n"
"/close - Close lobby\n"
"/open - Open lobby\n"
"/enable_translations - Translate relevant texts into all "
"languages spoken in a game\n"
"/disable_translations - Use English for those texts\n\n"
"<b>Experimental:</b> Play in multiple groups at the same time. Press the "
"<code>Current game: ...</code> button and select the group you want to play "
"a card in.\n"
"If you enjoy this bot, <a href=\"https://telegram.me/storebot?"
"start=mau_mau_bot\">rate me</a>, join the <a href=\"https://telegram.me/"
"unobotupdates\">update channel</a> and buy an UNO card game."
msgstr ""
#: bot.py:88
msgid "This bot is Free Software and licensed under the AGPL. The code is available "
"here: \n"
"https://github.com/jh0ker/mau_mau_bot"
msgstr ""
#: bot.py:133
msgid "Created a new game! Join the game with /join and start the game with /start"
msgstr ""
#: bot.py
msgid "Send this command in a group to be notified when a new game is started there."
msgstr ""
#: bot.py
msgid "A new game has been started in {title}"
msgstr ""
#: bot.py:152
msgid "The lobby is closed"
msgstr ""
#: bot.py:156
msgid "No game is running at the moment. Create a new game with /new"
msgstr ""
#: bot.py:162
msgid "You already joined the game. Start the game with /start"
msgstr ""
#: bot.py:167
msgid "Joined the game"
msgstr ""
#: bot.py:179 bot.py:191
msgid "You are not playing in a game in this group."
msgstr ""
#: bot.py:197 bot.py:258 bot.py:595
msgid "Game ended!"
msgstr ""
#: bot.py:201
msgid "Okay. Next Player: {name}"
msgstr ""
#: bot.py:219
msgid "Game not found."
msgstr ""
#: bot.py:223
msgid "Back to last group"
msgstr ""
#: bot.py:227
msgid "Please switch to the group you selected!"
msgstr ""
#: bot.py:233
#, python-format
msgid "Selected group: {group}\n"
"<b>Make sure that you switch to the correct group!</b>"
msgstr ""
#: bot.py:260
#, python-format
msgid "Removing {name} from the game"
msgstr ""
#: bot.py:273
msgid "There is no game running in this chat. Create a new one with /new"
msgstr ""
#: bot.py:278
msgid "The game has already started"
msgstr ""
#: bot.py:281
msgid "At least two players must /join the game before you can start it"
msgstr ""
#: bot.py:297
#, python-format, fuzzy
msgid "First player: {name}\n"
"Use /close to stop people from joining the game.\n"
"Enable multi-translations with /enable_translations"
msgstr ""
#: bot.py:321
msgid "Please select the group you want to play in."
msgstr ""
#: bot.py:335 bot.py:361
msgid "There is no running game in this chat."
msgstr ""
#: bot.py:342
msgid "Closed the lobby. No more players can join this game."
msgstr ""
#: bot.py:348 bot.py:373
#, python-format
msgid "Only the game creator ({name}) can do that."
msgstr ""
#: bot.py:349
#, python-format
msgid "Enabled multi-translations. Disable with /disable_translations"
msgstr ""
#: bot.py:377
#, python-format
msgid "Disabled multi-translations. Enable them again with /enable_translations"
msgstr ""
#: bot.py:368
msgid "Opened the lobby. New players may /join the game."
msgstr ""
#: bot.py:386
msgid "You are not playing in a game in this chat."
msgstr ""
#: bot.py:400
#, python-format
msgid "Please wait {time} second"
msgid_plural "Please wait {time} seconds"
msgstr[0] ""
msgstr[1] ""
#: bot.py:413
#, python-format
msgid "Waiting time to skip this player has been reduced to {time} second.\n"
"Next player: {name}"
msgid_plural "Waiting time to skip this player has been reduced to {time} seconds.\n"
"Next player: {name}"
msgstr[0] ""
msgstr[1] ""
#: bot.py:424
#, python-format
msgid "{name1} was skipped four times in a row and has been removed from the game.\n"
"Next player: {name2}"
msgstr ""
#: bot.py:432
#, python-format
msgid "{name} was skipped four times in a row and has been removed from the game.\n"
"The game ended."
msgstr ""
#: bot.py:455
msgid "All news here: https://telegram.me/unobotupdates"
msgstr ""
#: bot.py:513
#, python-format
msgid "Current game: {group}"
msgstr ""
#: bot.py:545
#, python-format
msgid "Cheat attempt by {name}"
msgstr ""
#: bot.py:562
msgid "Next player: {name}"
msgstr ""
#: bot.py:572
#, python-format
msgid "Waiting time for {name} has been reset to 90 seconds"
msgstr ""
#: bot.py:585
msgid "Please choose a color"
msgstr ""
#: bot.py:591
#, python-format
msgid "{name} won!"
msgstr ""
#: bot.py:613 bot.py:635 bot.py:647
msgid "There are no more cards in the deck."
msgstr ""
#: bot.py:627
#, python-format
msgid "Bluff called! Giving 4 cards to {name}"
msgstr ""
#: bot.py:639
#, python-format
msgid "{name1} didn't bluff! Giving 6 cards to {name2}"
msgstr ""
#: results.py:38
msgid "Choose Color"
msgstr ""
#: results.py:56
msgid "Last card (tap for game state):"
msgid_plural "Cards (tap for game state):"
msgstr[0] ""
msgstr[1] ""
#: results.py:60 results.py:123 results.py:165
msgid "Current player: {name}"
msgstr ""
#: results.py:61 results.py:124 results.py:167
msgid "Last card: {card}"
msgstr ""
#: results.py:62 results.py:125 results.py:168
msgid "Player: {player_list}"
msgid_plural "Players: {player_list}"
msgstr[0] ""
msgstr[1] ""
#: results.py:72
#, python-format
msgid "{name} ({number} card)"
msgid_plural "{name} ({number} cards)"
msgstr[0] ""
msgstr[1] ""
#: results.py:81
msgid "You are not playing"
msgstr ""
#: results.py:83
msgid "Not playing right now. Use /new to start a game or /join to join the current "
"game in this group"
msgstr ""
#: results.py:95
msgid "The game wasn't started yet"
msgstr ""
#: results.py:97
msgid "Start the game with /start"
msgstr ""
#: results.py:108
#, python-format
msgid "Drawing {number} card"
msgid_plural "Drawing {number} cards"
msgstr[0] ""
msgstr[1] ""
#: results.py:136
msgid "Pass"
msgstr ""
#: results.py:148
msgid "I'm calling your bluff!"
msgstr ""
#: settings.py:39
msgid "Please edit your settings in a private chat with the bot."
msgstr ""
#: settings.py:49
msgid "Enable statistics"
msgstr ""
#: settings.py:51
msgid "Delete all statistics"
msgstr ""
#: settings.py:53
msgid "Language"
msgstr ""
#: settings.py:54
msgid "Settings"
msgstr ""
#: settings.py:68
msgid "Enabled statistics!"
msgstr ""
#: settings.py:70
msgid "Select locale"
msgstr ""
#: settings.py:81
msgid "Deleted and disabled statistics!"
msgstr ""
#: settings.py:94
msgid "Set locale!"
msgstr ""
#: simple_commands.py
msgid "You did not enable statistics. Use /settings in "
"a private chat with the bot to enable them."
msgstr ""
#: simple_commands.py
msgid "{number} game played"
msgid_plural "{number} games played"
msgstr[0] ""
msgstr[1] ""
#: simple_commands.py
msgid "{number} first place"
msgid_plural "{number} first places"
msgstr[0] ""
msgstr[1] ""
#: simple_commands.py
msgid "{number} card played"
msgid_plural "{number} cards played"
msgstr[0] ""
msgstr[1] ""
#: utils.py
msgid "{emoji} Green"
msgstr ""
#: utils.py
msgid "{emoji} Red"
msgstr ""
#: utils.py
msgid "{emoji} Blue"
msgstr ""
#: utils.py
msgid "{emoji} Yellow"
msgstr ""

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
@ -20,9 +21,8 @@
import logging
from datetime import datetime
from telegram import User
import card as c
from errors import DeckEmptyError
class Player(object):
@ -33,12 +33,21 @@ class Player(object):
other players by placing itself behind the current player.
"""
def __init__(self, game, user, ai=False):
def __init__(self, game, user):
self.cards = list()
self.game = game
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
@ -50,21 +59,14 @@ class Player(object):
self._prev = self
game.current_player = self
for i in range(7):
self.cards.append(self.game.deck.draw())
self.bluffing = False
self.drew = False
self.anti_cheat = 0
self.turn_started = datetime.now()
self.waiting_time = 90
self.ai = ai
if ai and not user:
self.user = User(-1, "Computer")
def leave(self):
""" Leave the current game """
"""Removes player from the game and closes the gap in the list"""
if self.next is self:
return
@ -79,10 +81,10 @@ class Player(object):
self.cards = list()
def __repr__(self):
return repr(self.user) if not self.ai else "computer"
return repr(self.user)
def __str__(self):
return str(self.user) if not self.ai else "Computer"
return str(self.user)
@property
def next(self):
@ -106,8 +108,28 @@ class Player(object):
else:
self._next = player
def draw(self):
"""Draws 1+ cards from the deck, depending on the draw counter"""
_amount = self.game.draw_counter or 1
try:
for i in range(_amount):
self.cards.append(self.game.deck.draw())
except DeckEmptyError:
raise
finally:
self.game.draw_counter = 0
self.drew = True
def play(self, card):
"""Plays a card and removes it from hand"""
self.cards.remove(card)
self.game.play_card(card)
def playable_cards(self):
""" Returns a list of the cards this player can play right now """
"""Returns a list of the cards this player can play right now"""
playable = list()
last = self.game.last_card
@ -121,7 +143,7 @@ class Player(object):
# You may only play a +4 if you have no cards of the correct color
self.bluffing = False
for card in cards:
if self.card_playable(card, playable):
if self._card_playable(card):
self.logger.debug("Matching!")
playable.append(card)
@ -133,8 +155,8 @@ class Player(object):
return playable
def card_playable(self, card, playable):
""" Check a single card if it can be played """
def _card_playable(self, card):
"""Check a single card if it can be played"""
is_playable = True
last = self.game.last_card
@ -155,9 +177,8 @@ class Player(object):
(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 or card in playable:
self.logger.debug("Last card has no color or the card was "
"already added to the list")
elif not last.color:
self.logger.debug("Last card has no color")
is_playable = False
return is_playable

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
python-telegram-bot==5
pony

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,129 +18,135 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Defines helper functions to build the inline result list"""
from uuid import uuid4
from telegram import InlineQueryResultArticle, InputTextMessageContent, \
InlineQueryResultCachedSticker as Sticker
import card as c
from utils import *
from utils import display_color, display_color_group, display_name
from internationalization import _, __
def add_choose_color(results):
def add_choose_color(results, game):
"""Add choose color options"""
for color in c.COLORS:
results.append(
InlineQueryResultArticle(
id=color,
title="Choose Color",
title=_("選擇顏色"),
description=display_color(color),
input_message_content=
InputTextMessageContent(display_color(color))
InputTextMessageContent(display_color_group(color, game))
)
)
def add_other_cards(playable, player, results, game):
if not playable:
playable = list()
players = player_list(game)
def add_other_cards(player, results, game):
"""Add hand cards when choosing colors"""
results.append(
InlineQueryResultArticle(
"hand",
title="Cards (tap for game state):",
description=', '.join([repr(card) for card in
list_subtract(player.cards, playable)]),
input_message_content=InputTextMessageContent(
"Current player: " + display_name(game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players))
title=_("你的手牌(點一下以獲得遊戲狀態):",
"你的手牌(點一下以獲得遊戲狀態):",
len(player.cards)),
description=', '.join([repr(card) for card in player.cards]),
input_message_content=game_info(game)
)
)
def player_list(game):
players = list()
for player in game.players:
add_player(player, players)
return players
"""Generate list of player strings"""
return [_("{name}(有 {number} 張牌)",
"{name}(有 {number} 張牌)",
len(player.cards))
.format(name=player.user.first_name, number=len(player.cards))
for player in game.players]
def add_no_game(results):
"""Add text result if user is not playing"""
results.append(
InlineQueryResultArticle(
"nogame",
title="You are not playing",
title=_("你沒有在遊戲中"),
input_message_content=
InputTextMessageContent('Not playing right now. Use /new to start '
'a game or /join to join the current game '
'in this group')
InputTextMessageContent(_('你目前沒有在遊玩,使用 /new 開始新的遊戲或使用 /join 加入目前的遊戲'))
)
)
def add_not_started(results):
"""Add text result if the game has not yet started"""
results.append(
InlineQueryResultArticle(
"nogame",
title="The game wasn't started yet",
title=_("遊戲尚未開始"),
input_message_content=
InputTextMessageContent('Start the game with /start')
InputTextMessageContent(_('使用 /start 開始遊戲'))
)
)
def add_draw(player, results):
"""Add option to draw"""
n = player.game.draw_counter or 1
results.append(
Sticker(
"draw", sticker_file_id=c.STICKERS['option_draw'],
input_message_content=
InputTextMessageContent('Drawing %d card(s)'
% (player.game.draw_counter or 1))
InputTextMessageContent(__('{number} 張牌',
'{number} 張牌', n,
multi=player.game.translate)
.format(number=n))
)
)
def add_gameinfo(game, results):
players = player_list(game)
"""Add option to show game info"""
results.append(
Sticker(
"gameinfo",
sticker_file_id=c.STICKERS['option_info'],
input_message_content=InputTextMessageContent(
"Current player: " + display_name(game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players))
input_message_content=game_info(game)
)
)
def add_pass(results):
def add_pass(results, game):
"""Add option to pass"""
results.append(
Sticker(
"pass", sticker_file_id=c.STICKERS['option_pass'],
input_message_content=InputTextMessageContent('Pass')
input_message_content=InputTextMessageContent(
__('Pass下一位', multi=game.translate)
)
)
)
def add_call_bluff(results):
def add_call_bluff(results, game):
"""Add option to call a bluff"""
results.append(
Sticker(
"call_bluff",
sticker_file_id=c.STICKERS['option_bluff'],
input_message_content=
InputTextMessageContent("I'm calling your bluff!")
InputTextMessageContent(__("你作弊,一定還有其他牌可以出!",
multi=game.translate))
)
)
def add_play_card(game, card, results, can_play):
players = player_list(game)
def add_card(game, card, results, can_play):
"""Add an option that represents a card"""
if can_play:
results.append(
@ -148,16 +155,20 @@ def add_play_card(game, card, results, can_play):
else:
results.append(
Sticker(str(uuid4()), sticker_file_id=c.STICKERS_GREY[str(card)],
input_message_content=InputTextMessageContent(
"Current player: " + display_name(
game.current_player.user) +
"\n" +
"Last card: " + repr(game.last_card) + "\n" +
"Players: " + " -> ".join(players)))
input_message_content=game_info(game))
)
def add_player(itplayer, players):
players.append(itplayer.user.first_name + " (%d cards)"
% len(itplayer.cards))
def game_info(game):
players = player_list(game)
return InputTextMessageContent(
_("目前玩家:{name}")
.format(name=display_name(game.current_player.user)) +
"\n" +
_("最後的牌:{card}").format(card=repr(game.last_card)) +
"\n" +
_("玩家列表:{player_list}",
"玩家列表:{player_list}",
len(players))
.format(player_list=" -> ".join(players))
)

105
settings.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram import ReplyKeyboardMarkup, Emoji
from telegram.ext import CommandHandler, RegexHandler
from utils import send_async
from user_setting import UserSetting
from shared_vars import dispatcher
from locales import available_locales
from internationalization import _, user_locale
@user_locale
def show_settings(bot, update):
chat = update.message.chat
if update.message.chat.type != 'private':
send_async(bot, chat.id,
text=_("請私訊我來修改你的設定"))
return
us = UserSetting.get(id=update.message.from_user.id)
if not us:
us = UserSetting(id=update.message.from_user.id)
if not us.stats:
stats = Emoji.BAR_CHART + ' ' + _("啟用統計資料")
else:
stats = Emoji.CROSS_MARK + ' ' + _("刪除所有統計資料")
kb = [[stats], [Emoji.EARTH_GLOBE_EUROPE_AFRICA + ' ' + _("語言")]]
send_async(bot, chat.id, text=Emoji.WRENCH + ' ' + _("設定"),
reply_markup=ReplyKeyboardMarkup(keyboard=kb,
one_time_keyboard=True))
@user_locale
def kb_select(bot, update, groups):
chat = update.message.chat
user = update.message.from_user
option = groups[0]
if option == Emoji.BAR_CHART:
us = UserSetting.get(id=user.id)
us.stats = True
send_async(bot, chat.id, text=_("已啟用統計資料!"))
elif option == Emoji.EARTH_GLOBE_EUROPE_AFRICA:
kb = [[locale + ' - ' + descr]
for locale, descr
in sorted(available_locales.items())]
send_async(bot, chat.id, text=_("選擇語言"),
reply_markup=ReplyKeyboardMarkup(keyboard=kb,
one_time_keyboard=True))
elif option == Emoji.CROSS_MARK:
us = UserSetting.get(id=user.id)
us.stats = False
us.first_places = 0
us.games_played = 0
us.cards_played = 0
send_async(bot, chat.id, text=_("已停用並刪除統計資料!"))
@user_locale
def locale_select(bot, update, groups):
chat = update.message.chat
user = update.message.from_user
option = groups[0]
if option in available_locales:
us = UserSetting.get(id=user.id)
us.lang = option
_.push(option)
send_async(bot, chat.id, text=_("已設定所選擇的語言"))
_.pop()
def register():
dispatcher.add_handler(CommandHandler('settings', show_settings))
dispatcher.add_handler(RegexHandler('^([' + Emoji.BAR_CHART +
Emoji.EARTH_GLOBE_EUROPE_AFRICA +
Emoji.CROSS_MARK + ']) .+$',
kb_select, pass_groups=True))
dispatcher.add_handler(RegexHandler(r'^(\w\w_\w\w) - .*',
locale_select, pass_groups=True))

37
shared_vars.py Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram.ext import Updater
from telegram.contrib.botan import Botan
from game_manager import GameManager
from database import db
from credentials import TOKEN, BOTAN_TOKEN
db.bind('sqlite', 'uno.sqlite3', create_db=True)
db.generate_mapping(create_tables=True)
gm = GameManager()
updater = Updater(token=TOKEN, workers=32)
dispatcher = updater.dispatcher
botan = False
if BOTAN_TOKEN:
botan = Botan(BOTAN_TOKEN)

124
simple_commands.py Normal file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from telegram import ParseMode
from telegram.ext import CommandHandler
from user_setting import UserSetting
from utils import send_async
from shared_vars import dispatcher
from internationalization import _, user_locale
help_text = ("請跟隨以下指示:\n\n"
"1. 將這個機器人新增到群組\n"
"2. 輸入 /new 開始一個新遊戲或輸入 /join 加入一個運行中的遊戲\n"
"3. 當有兩個或以上的玩家加入,就可以用 /start 開始遊戲\n"
"4. 輸入 <code>@unopl_bot</code> 然後按<b>空白鍵</b>或點一下 "
"<code>via @unopl_bot</code> 或 <code>透過 @unopl_bot</code>,你便會看見你手上的牌或者是其他選項,例如:抽牌和"
"<b>?</b>(查看遊戲狀態)。然而<b>灰色的牌</b>就是<b>你現在不能使用的牌</b>,選擇其中一個"
"選項來執行對應的操作。\n"
"玩家可以在任何時間加入,如果想離開,請打 /leave 。如果玩家90秒後都沒有出牌"
",你可以用 /skip 跳過他。輸入 /notify_me 以讓我在本群組遊戲開始時私訊你\n"
"\n"
"<b>語言</b>和其他設定: /settings\n"
"遊戲創始人可以用以下的指令:\n"
"/close - 令其他人不可以中途加入\n"
"/open - 令其他人可以中途加入\n"
"<b>提醒:</b> 因為一些原因,您無法同時在不同的群組中遊玩。\n"
"如果你喜歡這個 bot你可以選擇買一副 UNO 牌來支持 UNO")
source_text = ("這個 bot 是一個免費的軟體,根據 AGPL 授權 \n"
"原始碼可以在這裡找到:\n"
"https://github.com/PinLin/mau_mau_bot\n"
"I fork from:\n"
"https://github.com/jh0ker/mau_mau_bot\n"
"Thanks very much!")
attributions = ("Attributions:\n"
'Draw icon by '
'<a href="http://www.faithtoken.com/">Faithtoken</a>\n'
'Pass icon by '
'<a href="http://delapouite.com/">Delapouite</a>\n'
"Originals available on http://game-icons.net\n"
"Icons edited by ɳick")
@user_locale
def help(bot, update):
"""Handler for the /help command"""
send_async(bot, update.message.chat_id, text=_(help_text),
parse_mode=ParseMode.HTML, disable_web_page_preview=True)
@user_locale
def source(bot, update):
"""Handler for the /help command"""
send_async(bot, update.message.chat_id, text=_(source_text) + '\n' +
_(attributions),
parse_mode=ParseMode.HTML, disable_web_page_preview=True)
@user_locale
def news(bot, update):
"""Handler for the /news command"""
send_async(bot, update.message.chat_id,
text=_("All news here: https://telegram.me/unobotupdates"),
disable_web_page_preview=True)
@user_locale
def stats(bot, update):
user = update.message.from_user
us = UserSetting.get(id=user.id)
if not us or not us.stats:
send_async(bot, update.message.chat_id,
text=_("您尚未啟用統計資料,請私我 "
"<code>/settings</code> 來啟用他", parse_mode=ParseMode.HTML))
else:
stats_text = list()
n = us.games_played
stats_text.append(
_("已遊玩 {number} 個遊戲",
"已遊玩 {number} 個遊戲",
n).format(number=n)
)
n = us.first_places
stats_text.append(
_("拿了 {number} 次第一名",
"拿了 {number} 次第一名",
n).format(number=n)
)
n = us.cards_played
stats_text.append(
_("已遊玩 {number} 張牌",
"已遊玩 {number} 張牌",
n).format(number=n)
)
send_async(bot, update.message.chat_id,
text='\n'.join(stats_text))
def register():
dispatcher.add_handler(CommandHandler('help', help))
dispatcher.add_handler(CommandHandler('source', source))
# dispatcher.add_handler(CommandHandler('news', news))
dispatcher.add_handler(CommandHandler('stats', stats))

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>

View File

@ -1,41 +0,0 @@
import unittest
from game import Game
from player import Player
class Test(unittest.TestCase):
game = None
def setUp(self):
self.game = Game()
def test_insert(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.assertEqual(p0, p2.next)
self.assertEqual(p1, p0.next)
self.assertEqual(p2, p1.next)
self.assertEqual(p0.prev, p2)
self.assertEqual(p1.prev, p0)
self.assertEqual(p2.prev, p1)
def test_reverse(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.game.reverse()
p3 = Player(self.game, "Player 3")
self.assertEqual(p0, p3.next)
self.assertEqual(p1, p2.next)
self.assertEqual(p2, p0.next)
self.assertEqual(p3, p1.next)
self.assertEqual(p0, p2.prev)
self.assertEqual(p1, p3.prev)
self.assertEqual(p2, p1.prev)
self.assertEqual(p3, p0.prev)

111
test/test_game_manager.py Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from telegram import User, Chat
from game_manager import GameManager
from errors import AlreadyJoinedError, LobbyClosedError, NoGameInChatError, \
NotEnoughPlayersError
class Test(unittest.TestCase):
game = None
def setUp(self):
self.gm = GameManager()
self.chat0 = Chat(0, 'group')
self.chat1 = Chat(1, 'group')
self.chat2 = Chat(2, 'group')
self.user0 = User(0, 'user0')
self.user1 = User(1, 'user1')
self.user2 = User(2, 'user2')
def test_new_game(self):
g0 = self.gm.new_game(self.chat0)
g1 = self.gm.new_game(self.chat1)
self.assertListEqual(self.gm.chatid_games[0], [g0])
self.assertListEqual(self.gm.chatid_games[1], [g1])
def test_join_game(self):
self.assertRaises(NoGameInChatError,
self.gm.join_game,
*(self.user0, self.chat0))
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.assertEqual(len(g0.players), 1)
self.gm.join_game(self.user1, self.chat0)
self.assertEqual(len(g0.players), 2)
g0.open = False
self.assertRaises(LobbyClosedError,
self.gm.join_game,
*(self.user2, self.chat0))
g0.open = True
self.assertRaises(AlreadyJoinedError,
self.gm.join_game,
*(self.user1, self.chat0))
def test_leave_game(self):
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.gm.join_game(self.user1, self.chat0)
self.assertRaises(NotEnoughPlayersError,
self.gm.leave_game,
*(self.user1, self.chat0))
self.gm.join_game(self.user2, self.chat0)
self.gm.leave_game(self.user0, self.chat0)
self.assertRaises(NoGameInChatError,
self.gm.leave_game,
*(self.user0, self.chat0))
def test_end_game(self):
g0 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user0, self.chat0)
self.gm.join_game(self.user1, self.chat0)
self.assertEqual(len(self.gm.userid_players[0]), 1)
g1 = self.gm.new_game(self.chat0)
self.gm.join_game(self.user2, self.chat0)
self.gm.end_game(self.chat0, self.user0)
self.assertEqual(len(self.gm.chatid_games[0]), 1)
self.gm.end_game(self.chat0, self.user2)
self.assertFalse(0 in self.gm.chatid_games)
self.assertFalse(0 in self.gm.userid_players)
self.assertFalse(1 in self.gm.userid_players)
self.assertFalse(2 in self.gm.userid_players)

164
test/test_player.py Normal file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from game import Game
from player import Player
import card as c
class Test(unittest.TestCase):
game = None
def setUp(self):
self.game = Game(None)
def test_insert(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.assertEqual(p0, p2.next)
self.assertEqual(p1, p0.next)
self.assertEqual(p2, p1.next)
self.assertEqual(p0.prev, p2)
self.assertEqual(p1.prev, p0)
self.assertEqual(p2.prev, p1)
def test_reverse(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
self.game.reverse()
p3 = Player(self.game, "Player 3")
self.assertEqual(p0, p3.next)
self.assertEqual(p1, p2.next)
self.assertEqual(p2, p0.next)
self.assertEqual(p3, p1.next)
self.assertEqual(p0, p2.prev)
self.assertEqual(p1, p3.prev)
self.assertEqual(p2, p1.prev)
self.assertEqual(p3, p0.prev)
def test_leave(self):
p0 = Player(self.game, "Player 0")
p1 = Player(self.game, "Player 1")
p2 = Player(self.game, "Player 2")
p1.leave()
self.assertEqual(p0, p2.next)
self.assertEqual(p2, p0.next)
def test_draw(self):
p = Player(self.game, "Player 0")
deck_before = len(self.game.deck.cards)
top_card = self.game.deck.cards[-1]
p.draw()
self.assertEqual(top_card, p.cards[-1])
self.assertEqual(deck_before, len(self.game.deck.cards) + 1)
def test_draw_two(self):
p = Player(self.game, "Player 0")
deck_before = len(self.game.deck.cards)
self.game.draw_counter = 2
p.draw()
self.assertEqual(deck_before, len(self.game.deck.cards) + 2)
def test_playable_cards_simple(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, '5')
p.cards = [c.Card(c.RED, '0'), c.Card(c.RED, '5'), c.Card(c.BLUE, '0'),
c.Card(c.GREEN, '5'), c.Card(c.GREEN, '8')]
expected = [c.Card(c.RED, '0'), c.Card(c.RED, '5'),
c.Card(c.GREEN, '5')]
self.assertListEqual(p.playable_cards(), expected)
def test_playable_cards_on_draw_two(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, c.DRAW_TWO)
self.game.draw_counter = 2
p.cards = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.RED, '5'),
c.Card(c.BLUE, '0'), c.Card(c.GREEN, '5'),
c.Card(c.GREEN, c.DRAW_TWO)]
expected = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.GREEN, c.DRAW_TWO)]
self.assertListEqual(p.playable_cards(), expected)
def test_playable_cards_on_draw_four(self):
p = Player(self.game, "Player 0")
self.game.last_card = c.Card(c.RED, None, c.DRAW_FOUR)
self.game.draw_counter = 4
p.cards = [c.Card(c.RED, c.DRAW_TWO), c.Card(c.RED, '5'),
c.Card(c.BLUE, '0'), c.Card(c.GREEN, '5'),
c.Card(c.GREEN, c.DRAW_TWO),
c.Card(None, None, c.DRAW_FOUR),
c.Card(None, None, c.CHOOSE)]
expected = list()
self.assertListEqual(p.playable_cards(), expected)
def test_bluffing(self):
p = Player(self.game, "Player 0")
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)

32
user_setting.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telegram bot to play UNO in group chats
# Copyright (c) 2016 Jannes Höke <uno@jhoeke.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from database import db, Optional, Required, PrimaryKey, db_session
class UserSetting(db.Entity):
id = PrimaryKey(int, auto=False, size=64) # Telegram User ID
lang = Optional(str, default='') # The language setting for this user
stats = Optional(bool, default=False) # Opt-in to keep game statistics
first_places = Optional(int, default=0) # Nr. of games won in first place
games_played = Optional(int, default=0) # Nr. of games completed
cards_played = Optional(int, default=0) # Nr. of cards played total
use_keyboards = Optional(bool, default=False) # Use keyboards (unused)

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,7 +18,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from telegram import Emoji
from telegram.ext.dispatcher import run_async
from internationalization import _, __
logger = logging.getLogger(__name__)
TIMEOUT = 2.5
def list_subtract(list1, list2):
@ -30,22 +40,6 @@ def list_subtract(list1, list2):
return list(sorted(list1))
def list_subtract_unsorted(list1, list2):
""" Helper function to subtract two lists and return the sorted result """
list1 = list1.copy()
for x in list2:
try:
list1.remove(x)
except ValueError:
print(list1)
print(list2)
print(x)
raise
return list1
def display_name(user):
""" Get the current players name including their username, if possible """
user_name = user.first_name
@ -57,10 +51,55 @@ def display_name(user):
def display_color(color):
""" Convert a color code to actual color name """
if color == "r":
return Emoji.HEAVY_BLACK_HEART + " Red"
return _("{emoji} 紅色").format(emoji=Emoji.HEAVY_BLACK_HEART)
if color == "b":
return Emoji.BLUE_HEART + " Blue"
return _("{emoji} 藍色").format(emoji=Emoji.BLUE_HEART)
if color == "g":
return Emoji.GREEN_HEART + " Green"
return _("{emoji} 綠色").format(emoji=Emoji.GREEN_HEART)
if color == "y":
return Emoji.YELLOW_HEART + " Yellow"
return _("{emoji} 黃色").format(emoji=Emoji.YELLOW_HEART)
def display_color_group(color, game):
""" Convert a color code to actual color name """
if color == "r":
return __("{emoji} 紅色", game.translate).format(
emoji=Emoji.HEAVY_BLACK_HEART)
if color == "b":
return __("{emoji} 藍色", game.translate).format(
emoji=Emoji.BLUE_HEART)
if color == "g":
return __("{emoji} 綠色", game.translate).format(
emoji=Emoji.GREEN_HEART)
if color == "y":
return __("{emoji} 黃色", game.translate).format(
emoji=Emoji.YELLOW_HEART)
def error(bot, update, error):
"""Simple error handler"""
logger.exception(error)
@run_async
def send_async(bot, *args, **kwargs):
"""Send a message asynchronously"""
if 'timeout' not in kwargs:
kwargs['timeout'] = TIMEOUT
try:
bot.sendMessage(*args, **kwargs)
except Exception as e:
error(None, None, e)
@run_async
def answer_async(bot, *args, **kwargs):
"""Answer an inline query asynchronously"""
if 'timeout' not in kwargs:
kwargs['timeout'] = TIMEOUT
try:
bot.answerInlineQuery(*args, **kwargs)
except Exception as e:
error(None, None, e)