Compare commits

..

No commits in common. "new_rule" and "master" have entirely different histories.

644 changed files with 330 additions and 2430 deletions

View file

@ -1,60 +0,0 @@
name: Docker build
on:
schedule:
- cron: '25 15 * * *'
push:
branches:
- master
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

6
.gitignore vendored
View file

@ -27,8 +27,6 @@ var/
.installed.cfg .installed.cfg
*.egg *.egg
venv/
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.
@ -71,7 +69,3 @@ target/
# Database file # Database file
uno.sqlite3 uno.sqlite3
images/api_auth.json
images/sticker_config.json
images/sticker_uploader.session

View file

@ -10,6 +10,7 @@ COPY . .
RUN cd locales && find . -maxdepth 2 -type d -name 'LC_MESSAGES' -exec ash -c 'msgfmt {}/unobot.po -o {}/unobot.mo' \; RUN cd locales && find . -maxdepth 2 -type d -name 'LC_MESSAGES' -exec ash -c 'msgfmt {}/unobot.po -o {}/unobot.mo' \;
RUN pip install -r requirements.txt
VOLUME /app/data VOLUME /app/data
ENV UNO_DB /app/data/uno.sqlite3 ENV UNO_DB /app/data/uno.sqlite3

View file

@ -4,11 +4,10 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages] [dev-packages]
telethon = "*"
[packages] [packages]
python-telegram-bot = "==13.11" python-telegram-bot = "==8.1.1"
pony = "*" pony = "*"
[requires] [requires]
python_version = "3.11" python_version = "3.7"

150
Pipfile.lock generated
View file

@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "87f82f4abefdefd3b212fa99f5cbf6e222d6855aa7574d7a94fbf51b33cc342f" "sha256": "de56c4d5f516205e99d141cd7d372f67b602b6f981306971c01ffe25a5abf5c6"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.11" "python_version": "3.7"
}, },
"sources": [ "sources": [
{ {
@ -16,150 +16,34 @@
] ]
}, },
"default": { "default": {
"apscheduler": {
"hashes": [
"sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244",
"sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"
],
"version": "==3.6.3"
},
"cachetools": {
"hashes": [
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
],
"markers": "python_version ~= '3.5'",
"version": "==4.2.2"
},
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
], ],
"markers": "python_version >= '3.6'", "version": "==2019.6.16"
"version": "==2022.12.7" },
"future": {
"hashes": [
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
],
"version": "==0.17.1"
}, },
"pony": { "pony": {
"hashes": [ "hashes": [
"sha256:5f45fc67587f4520c560a57148cc573b097d42f82f5cb200d72c957b5708198d", "sha256:55bb9d4d12029d8c2bbbc7a284970e72225035db7e6370c0a15ec93d1886fe88"
"sha256:608a1c1d662983bad2590e650f2bbc1cd6ed48558894ad8f50da4739ff98f614"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.7.16" "version": "==0.7.10"
}, },
"python-telegram-bot": { "python-telegram-bot": {
"hashes": [ "hashes": [
"sha256:534f5bb0ff4ca34c9252e97e0b3bcdab81d97be0eb4821682a361cb426c00e55", "sha256:238c4a88b09d93c52d413bcf7e7fe14dfeb02f5f9222ffe4cafd4bd3d55489a3",
"sha256:baeff704baa2ac3dc17a944c02da888228ad258e89be2e5bcbd13a8a5102d573" "sha256:997983e5082dc6aa811bce3a6014731201fc64b0a9c02fdb26beac686029d94b"
], ],
"index": "pypi", "index": "pypi",
"version": "==13.11" "version": "==8.1.1"
},
"pytz": {
"hashes": [
"sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588",
"sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"
],
"version": "==2023.3"
},
"pytz-deprecation-shim": {
"hashes": [
"sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6",
"sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==0.1.0.post0"
},
"setuptools": {
"hashes": [
"sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a",
"sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"
],
"markers": "python_version >= '3.7'",
"version": "==67.6.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"tornado": {
"hashes": [
"sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca",
"sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72",
"sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23",
"sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8",
"sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b",
"sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9",
"sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13",
"sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75",
"sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac",
"sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e",
"sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"
],
"markers": "python_version >= '3.7'",
"version": "==6.2"
},
"tzdata": {
"hashes": [
"sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a",
"sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"
],
"markers": "platform_system == 'Windows'",
"version": "==2023.3"
},
"tzlocal": {
"hashes": [
"sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355",
"sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"
],
"markers": "python_version >= '3.7'",
"version": "==4.3"
} }
}, },
"develop": { "develop": {}
"pyaes": {
"hashes": [
"sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"
],
"version": "==1.6.1"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"rsa": {
"hashes": [
"sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
"sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
],
"markers": "python_version >= '3.6' and python_version < '4'",
"version": "==4.9"
},
"telethon": {
"hashes": [
"sha256:613bae42acb5f2eeb1a0b92614e323021c66f374db62adf9826ea0c2c9120bb1",
"sha256:893c10f133974fba4c53eb1736b6514d596d1cd94c83436a711f3345df945199"
],
"index": "pypi",
"version": "==1.28.2"
}
}
} }

View file

@ -3,7 +3,7 @@
The following awesome people contributed to this project by translating it: The following awesome people contributed to this project by translating it:
| Locale | Translators | | Locale | Translators |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | |--------|-------------|
| ca_CA | [retiolus](https://github.com/retiolus) | | ca_CA | [retiolus](https://github.com/retiolus) |
| de_DE | [Jannes Höke](https://github.com/jh0ker) | | 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 | | es_ES | [José.A Rojo](https://github.com/J4RV), [Ricardo Valverde Hernández](https://telegram.me/rivh1), Victor, Yuga |
@ -11,7 +11,6 @@ The following awesome people contributed to this project by translating it:
| it_IT | Carola Mariano, ɳick | | it_IT | Carola Mariano, ɳick |
| ml_IN | [Adhith T](https://github.com/adhitht123) | | ml_IN | [Adhith T](https://github.com/adhitht123) |
| pt_BR | [Iuri Guilherme](https://github.com/iuriguilherme), [João Rodrigo Couto de Oliveira](http://twitter.com/JoaoRodrigoJR) | | pt_BR | [Iuri Guilherme](https://github.com/iuriguilherme), [João Rodrigo Couto de Oliveira](http://twitter.com/JoaoRodrigoJR) |
| vi_VN | [Lê Minh Sơn](https://github.com/leminhson06) |
| zh_CN | [imlonghao](https://github.com/imlonghao), [XhyEax](https://github.com/XhyEax) | | zh_CN | [imlonghao](https://github.com/imlonghao), [XhyEax](https://github.com/XhyEax) |
| zh_HK | [Jed Cheng](https://www.facebook.com/profile.php?id=100002258388821) | | 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) | | zh_TW | [Eugene Lam](https://www.facebook.com/eugenelam1118), [jimchen5209](https://www.youtube.com/user/jimchen5209), [pan93412](https://www.github.com/pan93412) |

View file

@ -6,8 +6,6 @@ import card as c
from datetime import datetime from datetime import datetime
from telegram import Message, Chat from telegram import Message, Chat
from telegram.ext import CallbackContext
from apscheduler.jobstores.base import JobLookupError
from config import TIME_REMOVAL_AFTER_SKIP, MIN_FAST_TURN_TIME from config import TIME_REMOVAL_AFTER_SKIP, MIN_FAST_TURN_TIME
from errors import DeckEmptyError, NotEnoughPlayersError from errors import DeckEmptyError, NotEnoughPlayersError
@ -113,7 +111,7 @@ def do_play_card(bot, player, result_id):
if us.stats: if us.stats:
us.games_played += 1 us.games_played += 1
if game.players_won == 0: if game.players_won is 0:
us.first_places += 1 us.first_places += 1
game.players_won += 1 game.players_won += 1
@ -155,15 +153,11 @@ def do_call_bluff(bot, player):
chat = game.chat chat = game.chat
if player.prev.bluffing: if player.prev.bluffing:
draw_prev = 4
draw_next = game.draw_counter - draw_prev
draw_next_text = ". " + __("Giving {count} cards to {name}").format(count=draw_next, name=player.user.first_name) if draw_next > 0 else ""
send_async(bot, chat.id, send_async(bot, chat.id,
text=__("Bluff called! Giving {count} cards to {name}" + draw_next_text, text=__("Bluff called! Giving 4 cards to {name}",
multi=game.translate) multi=game.translate)
.format(name=player.prev.user.first_name, count=draw_prev)) .format(name=player.prev.user.first_name))
game.draw_counter = draw_prev
try: try:
player.prev.draw() player.prev.draw()
except DeckEmptyError: except DeckEmptyError:
@ -171,21 +165,12 @@ def do_call_bluff(bot, player):
text=__("There are no more cards in the deck.", text=__("There are no more cards in the deck.",
multi=game.translate)) multi=game.translate))
game.draw_counter = draw_next
try:
player.draw()
except DeckEmptyError:
send_async(bot, player.game.chat.id,
text=__("There are no more cards in the deck.",
multi=game.translate))
else: else:
game.draw_counter += 2 game.draw_counter += 2
send_async(bot, chat.id, send_async(bot, chat.id,
text=__("{name1} didn't bluff! Giving {count} cards to {name2}", text=__("{name1} didn't bluff! Giving 6 cards to {name2}",
multi=game.translate) multi=game.translate)
.format(name1=player.prev.user.first_name, .format(name1=player.prev.user.first_name,
count=game.draw_counter,
name2=player.user.first_name)) name2=player.user.first_name))
try: try:
player.draw() player.draw()
@ -206,10 +191,7 @@ def start_player_countdown(bot, game, job_queue):
if game.mode == 'fast': if game.mode == 'fast':
if game.job: if game.job:
try:
game.job.schedule_removal() game.job.schedule_removal()
except JobLookupError:
pass
job = job_queue.run_once( job = job_queue.run_once(
#lambda x,y: do_skip(bot, player), #lambda x,y: do_skip(bot, player),
@ -223,9 +205,9 @@ def start_player_countdown(bot, game, job_queue):
player.game.job = job player.game.job = job
def skip_job(context: CallbackContext): def skip_job(bot, job):
player = context.job.context.player player = job.context.player
game = player.game game = player.game
if game_is_running(game): if game_is_running(game):
job_queue = context.job.context.job_queue job_queue = job.context.job_queue
do_skip(context.bot, player, job_queue) do_skip(bot, player, job_queue)

203
bot.py
View file

@ -21,9 +21,9 @@ import logging
from datetime import datetime from datetime import datetime
from telegram import ParseMode, InlineKeyboardMarkup, \ from telegram import ParseMode, InlineKeyboardMarkup, \
InlineKeyboardButton, Update InlineKeyboardButton
from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \ from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \
CommandHandler, MessageHandler, Filters, CallbackQueryHandler, CallbackContext CommandHandler, MessageHandler, Filters, CallbackQueryHandler
from telegram.ext.dispatcher import run_async from telegram.ext.dispatcher import run_async
import card as c import card as c
@ -49,10 +49,9 @@ logging.basicConfig(
level=logging.INFO level=logging.INFO
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.getLogger('apscheduler').setLevel(logging.WARNING)
@user_locale @user_locale
def notify_me(update: Update, context: CallbackContext): def notify_me(bot, update):
"""Handler for /notify_me command, pm people for next game""" """Handler for /notify_me command, pm people for next game"""
chat_id = update.message.chat_id chat_id = update.message.chat_id
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
@ -68,25 +67,15 @@ def notify_me(update: Update, context: CallbackContext):
@user_locale @user_locale
def new_game(update: Update, context: CallbackContext): def new_game(bot, update):
"""Handler for the /new command""" """Handler for the /new command"""
chat_id = update.message.chat_id chat_id = update.message.chat_id
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help_handler(update, context) help_handler(bot, update)
else: else:
try:
_game = gm.chatid_games[chat_id][-1]
except (KeyError, IndexError):
pass
else:
send_async(bot, chat_id,
text=_("There is already a game running in this chat. Join the "
"game with /join"))
return
if update.message.chat_id in gm.remind_dict: if update.message.chat_id in gm.remind_dict:
for user in gm.remind_dict[update.message.chat_id]: for user in gm.remind_dict[update.message.chat_id]:
send_async(bot, send_async(bot,
@ -100,88 +89,88 @@ def new_game(update: Update, context: CallbackContext):
game.starter = update.message.from_user game.starter = update.message.from_user
game.owner.append(update.message.from_user.id) game.owner.append(update.message.from_user.id)
game.mode = DEFAULT_GAMEMODE game.mode = DEFAULT_GAMEMODE
send_async(context.bot, chat_id, send_async(bot, chat_id,
text=_("Created a new game! Join the game with /join " text=_("Created a new game! Join the game with /join "
"and start the game with /start")) "and start the game with /start"))
@user_locale @user_locale
def kill_game(update: Update, context: CallbackContext): def kill_game(bot, update):
"""Handler for the /kill command""" """Handler for the /kill command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat.id) games = gm.chatid_games.get(chat.id)
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help_handler(update, context) help_handler(bot, update)
return return
if not games: if not games:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
game = games[-1] game = games[-1]
if user_is_creator_or_admin(user, game, context.bot, chat): if user_is_creator_or_admin(user, game, bot, chat):
try: try:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(bot, chat.id, text=__("Game ended!", multi=game.translate))
except NoGameInChatError: except NoGameInChatError:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("The game is not started yet. " text=_("The game is not started yet. "
"Join the game with /join and start the game with /start"), "Join the game with /join and start the game with /start"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@user_locale @user_locale
def join_game(update: Update, context: CallbackContext): def join_game(bot, update):
"""Handler for the /join command""" """Handler for the /join command"""
chat = update.message.chat chat = update.message.chat
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help_handler(update, context) help_handler(bot, update)
return return
try: try:
gm.join_game(update.message.from_user, chat) gm.join_game(update.message.from_user, chat)
except LobbyClosedError: except LobbyClosedError:
send_async(context.bot, chat.id, text=_("The lobby is closed")) send_async(bot, chat.id, text=_("The lobby is closed"))
except NoGameInChatError: except NoGameInChatError:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("No game is running at the moment. " text=_("No game is running at the moment. "
"Create a new game with /new"), "Create a new game with /new"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
except AlreadyJoinedError: except AlreadyJoinedError:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("You already joined the game. Start the game " text=_("You already joined the game. Start the game "
"with /start"), "with /start"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
except DeckEmptyError: except DeckEmptyError:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There are not enough cards left in the deck for " text=_("There are not enough cards left in the deck for "
"new players to join."), "new players to join."),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Joined the game"), text=_("Joined the game"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@user_locale @user_locale
def leave_game(update: Update, context: CallbackContext): def leave_game(bot, update):
"""Handler for the /leave command""" """Handler for the /leave command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
@ -189,7 +178,7 @@ def leave_game(update: Update, context: CallbackContext):
player = gm.player_for_user_in_chat(user, chat) player = gm.player_for_user_in_chat(user, chat)
if player is None: if player is None:
send_async(context.bot, chat.id, text=_("You are not playing in a game in " send_async(bot, chat.id, text=_("You are not playing in a game in "
"this group."), "this group."),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
@ -201,23 +190,23 @@ def leave_game(update: Update, context: CallbackContext):
gm.leave_game(user, chat) gm.leave_game(user, chat)
except NoGameInChatError: except NoGameInChatError:
send_async(context.bot, chat.id, text=_("You are not playing in a game in " send_async(bot, chat.id, text=_("You are not playing in a game in "
"this group."), "this group."),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
except NotEnoughPlayersError: except NotEnoughPlayersError:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(bot, chat.id, text=__("Game ended!", multi=game.translate))
else: else:
if game.started: if game.started:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=__("Okay. Next Player: {name}", text=__("Okay. Next Player: {name}",
multi=game.translate).format( multi=game.translate).format(
name=display_name(game.current_player.user)), name=display_name(game.current_player.user)),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=__("{name} left the game before it started.", text=__("{name} left the game before it started.",
multi=game.translate).format( multi=game.translate).format(
name=display_name(user)), name=display_name(user)),
@ -225,11 +214,11 @@ def leave_game(update: Update, context: CallbackContext):
@user_locale @user_locale
def kick_player(update: Update, context: CallbackContext): def kick_player(bot, update):
"""Handler for the /kick command""" """Handler for the /kick command"""
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help_handler(update, context) help_handler(bot, update)
return return
chat = update.message.chat chat = update.message.chat
@ -239,20 +228,20 @@ def kick_player(update: Update, context: CallbackContext):
game = gm.chatid_games[chat.id][-1] game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("No game is running at the moment. " text=_("No game is running at the moment. "
"Create a new game with /new"), "Create a new game with /new"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
if not game.started: if not game.started:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("The game is not started yet. " text=_("The game is not started yet. "
"Join the game with /join and start the game with /start"), "Join the game with /join and start the game with /start"),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
if user_is_creator_or_admin(user, game, context.bot, chat): if user_is_creator_or_admin(user, game, bot, chat):
if update.message.reply_to_message: if update.message.reply_to_message:
kicked = update.message.reply_to_message.from_user kicked = update.message.reply_to_message.from_user
@ -261,40 +250,40 @@ def kick_player(update: Update, context: CallbackContext):
gm.leave_game(kicked, chat) gm.leave_game(kicked, chat)
except NoGameInChatError: except NoGameInChatError:
send_async(context.bot, chat.id, text=_("Player {name} is not found in the current game.".format(name=display_name(kicked))), send_async(bot, chat.id, text=_("Player {name} is not found in the current game.".format(name=display_name(kicked))),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
except NotEnoughPlayersError: except NotEnoughPlayersError:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("{0} was kicked by {1}".format(display_name(kicked), display_name(user)))) text=_("{0} was kicked by {1}".format(display_name(kicked), display_name(user))))
send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(bot, chat.id, text=__("Game ended!", multi=game.translate))
return return
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("{0} was kicked by {1}".format(display_name(kicked), display_name(user)))) text=_("{0} was kicked by {1}".format(display_name(kicked), display_name(user))))
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Please reply to the person you want to kick and type /kick again."), text=_("Please reply to the person you want to kick and type /kick again."),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
return return
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=__("Okay. Next Player: {name}", text=__("Okay. Next Player: {name}",
multi=game.translate).format( multi=game.translate).format(
name=display_name(game.current_player.user)), name=display_name(game.current_player.user)),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
def select_game(update: Update, context: CallbackContext): def select_game(bot, update):
"""Handler for callback queries to select the current game""" """Handler for callback queries to select the current game"""
chat_id = int(update.callback_query.data) chat_id = int(update.callback_query.data)
@ -310,15 +299,16 @@ def select_game(update: Update, context: CallbackContext):
text=_("Game not found.")) text=_("Game not found."))
return return
def selected(): @run_async
def selected(bot):
back = [[InlineKeyboardButton(text=_("Back to last group"), back = [[InlineKeyboardButton(text=_("Back to last group"),
switch_inline_query='')]] switch_inline_query='')]]
context.bot.answerCallbackQuery(update.callback_query.id, bot.answerCallbackQuery(update.callback_query.id,
text=_("Please switch to the group you selected!"), text=_("Please switch to the group you selected!"),
show_alert=False, show_alert=False,
timeout=TIMEOUT) timeout=TIMEOUT)
context.bot.editMessageText(chat_id=update.callback_query.message.chat_id, bot.editMessageText(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id, message_id=update.callback_query.message.message_id,
text=_("Selected group: {group}\n" text=_("Selected group: {group}\n"
"<b>Make sure that you switch to the correct " "<b>Make sure that you switch to the correct "
@ -328,11 +318,11 @@ def select_game(update: Update, context: CallbackContext):
parse_mode=ParseMode.HTML, parse_mode=ParseMode.HTML,
timeout=TIMEOUT) timeout=TIMEOUT)
dispatcher.run_async(selected) selected(bot)
@game_locales @game_locales
def status_update(update: Update, context: CallbackContext): def status_update(bot, update):
"""Remove player from game if user leaves the group""" """Remove player from game if user leaves the group"""
chat = update.message.chat chat = update.message.chat
@ -347,17 +337,17 @@ def status_update(update: Update, context: CallbackContext):
pass pass
except NotEnoughPlayersError: except NotEnoughPlayersError:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(context.bot, chat.id, text=__("Game ended!", send_async(bot, chat.id, text=__("Game ended!",
multi=game.translate)) multi=game.translate))
else: else:
send_async(context.bot, chat.id, text=__("Removing {name} from the game", send_async(bot, chat.id, text=__("Removing {name} from the game",
multi=game.translate) multi=game.translate)
.format(name=display_name(user))) .format(name=display_name(user)))
@game_locales @game_locales
@user_locale @user_locale
def start_game(update: Update, context: CallbackContext): def start_game(bot, update, args, job_queue):
"""Handler for the /start command""" """Handler for the /start command"""
if update.message.chat.type != 'private': if update.message.chat.type != 'private':
@ -366,16 +356,16 @@ def start_game(update: Update, context: CallbackContext):
try: try:
game = gm.chatid_games[chat.id][-1] game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no game running in this chat. Create " text=_("There is no game running in this chat. Create "
"a new one with /new")) "a new one with /new"))
return return
if game.started: if game.started:
send_async(context.bot, chat.id, text=_("The game has already started")) send_async(bot, chat.id, text=_("The game has already started"))
elif len(game.players) < MIN_PLAYERS: elif len(game.players) < MIN_PLAYERS:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=__("At least {minplayers} players must /join the game " text=__("At least {minplayers} players must /join the game "
"before you can start it").format(minplayers=MIN_PLAYERS)) "before you can start it").format(minplayers=MIN_PLAYERS))
@ -393,29 +383,30 @@ def start_game(update: Update, context: CallbackContext):
multi=game.translate) multi=game.translate)
.format(name=display_name(game.current_player.user))) .format(name=display_name(game.current_player.user)))
@run_async
def send_first(): def send_first():
"""Send the first card and player""" """Send the first card and player"""
context.bot.sendSticker(chat.id, bot.sendSticker(chat.id,
sticker=c.STICKERS[str(game.last_card)], sticker=c.STICKERS[str(game.last_card)],
timeout=TIMEOUT) timeout=TIMEOUT)
context.bot.sendMessage(chat.id, bot.sendMessage(chat.id,
text=first_message, text=first_message,
reply_markup=InlineKeyboardMarkup(choice), reply_markup=InlineKeyboardMarkup(choice),
timeout=TIMEOUT) timeout=TIMEOUT)
dispatcher.run_async(send_first) send_first()
start_player_countdown(context.bot, game, context.job_queue) start_player_countdown(bot, game, job_queue)
elif len(context.args) and context.args[0] == 'select': elif len(args) and args[0] == 'select':
players = gm.userid_players[update.message.from_user.id] players = gm.userid_players[update.message.from_user.id]
groups = list() groups = list()
for player in players: for player in players:
title = player.game.chat.title title = player.game.chat.title
if player == gm.userid_current[update.message.from_user.id]: if player is gm.userid_current[update.message.from_user.id]:
title = '- %s -' % player.game.chat.title title = '- %s -' % player.game.chat.title
groups.append( groups.append(
@ -423,23 +414,23 @@ def start_game(update: Update, context: CallbackContext):
callback_data=str(player.game.chat.id))] callback_data=str(player.game.chat.id))]
) )
send_async(context.bot, update.message.chat_id, send_async(bot, update.message.chat_id,
text=_('Please select the group you want to play in.'), text=_('Please select the group you want to play in.'),
reply_markup=InlineKeyboardMarkup(groups)) reply_markup=InlineKeyboardMarkup(groups))
else: else:
help_handler(update, context) help_handler(bot, update)
@user_locale @user_locale
def close_game(update: Update, context: CallbackContext): def close_game(bot, update):
"""Handler for the /close command""" """Handler for the /close command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat.id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -447,12 +438,12 @@ def close_game(update: Update, context: CallbackContext):
if user.id in game.owner: if user.id in game.owner:
game.open = False game.open = False
send_async(context.bot, chat.id, text=_("Closed the lobby. " send_async(bot, chat.id, text=_("Closed the lobby. "
"No more players can join this game.")) "No more players can join this game."))
return return
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@ -460,14 +451,14 @@ def close_game(update: Update, context: CallbackContext):
@user_locale @user_locale
def open_game(update: Update, context: CallbackContext): def open_game(bot, update):
"""Handler for the /open command""" """Handler for the /open command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat.id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -475,11 +466,11 @@ def open_game(update: Update, context: CallbackContext):
if user.id in game.owner: if user.id in game.owner:
game.open = True game.open = True
send_async(context.bot, chat.id, text=_("Opened the lobby. " send_async(bot, chat.id, text=_("Opened the lobby. "
"New players may /join the game.")) "New players may /join the game."))
return return
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@ -487,14 +478,14 @@ def open_game(update: Update, context: CallbackContext):
@user_locale @user_locale
def enable_translations(update: Update, context: CallbackContext): def enable_translations(bot, update):
"""Handler for the /enable_translations command""" """Handler for the /enable_translations command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat.id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -502,12 +493,12 @@ def enable_translations(update: Update, context: CallbackContext):
if user.id in game.owner: if user.id in game.owner:
game.translate = True game.translate = True
send_async(context.bot, chat.id, text=_("Enabled multi-translations. " send_async(bot, chat.id, text=_("Enabled multi-translations. "
"Disable with /disable_translations")) "Disable with /disable_translations"))
return return
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@ -515,14 +506,14 @@ def enable_translations(update: Update, context: CallbackContext):
@user_locale @user_locale
def disable_translations(update: Update, context: CallbackContext): def disable_translations(bot, update):
"""Handler for the /disable_translations command""" """Handler for the /disable_translations command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
games = gm.chatid_games.get(chat.id) games = gm.chatid_games.get(chat.id)
if not games: if not games:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -530,13 +521,13 @@ def disable_translations(update: Update, context: CallbackContext):
if user.id in game.owner: if user.id in game.owner:
game.translate = False game.translate = False
send_async(context.bot, chat.id, text=_("Disabled multi-translations. " send_async(bot, chat.id, text=_("Disabled multi-translations. "
"Enable them again with " "Enable them again with "
"/enable_translations")) "/enable_translations"))
return return
else: else:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Only the game creator ({name}) and admin can do that.") text=_("Only the game creator ({name}) and admin can do that.")
.format(name=game.starter.first_name), .format(name=game.starter.first_name),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
@ -545,14 +536,14 @@ def disable_translations(update: Update, context: CallbackContext):
@game_locales @game_locales
@user_locale @user_locale
def skip_player(update: Update, context: CallbackContext): def skip_player(bot, update):
"""Handler for the /skip command""" """Handler for the /skip command"""
chat = update.message.chat chat = update.message.chat
user = update.message.from_user user = update.message.from_user
player = gm.player_for_user_in_chat(user, chat) player = gm.player_for_user_in_chat(user, chat)
if not player: if not player:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("You are not playing in a game in this chat.")) text=_("You are not playing in a game in this chat."))
return return
@ -567,19 +558,19 @@ def skip_player(update: Update, context: CallbackContext):
# You can skip yourself even if you have time left (you'll still draw) # You can skip yourself even if you have time left (you'll still draw)
if delta < skipped_player.waiting_time and player != skipped_player: if delta < skipped_player.waiting_time and player != skipped_player:
n = skipped_player.waiting_time - delta n = skipped_player.waiting_time - delta
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=_("Please wait {time} second", text=_("Please wait {time} second",
"Please wait {time} seconds", "Please wait {time} seconds",
n) n)
.format(time=n), .format(time=n),
reply_to_message_id=update.message.message_id) reply_to_message_id=update.message.message_id)
else: else:
do_skip(context.bot, player) do_skip(bot, player)
@game_locales @game_locales
@user_locale @user_locale
def reply_to_query(update: Update, context: CallbackContext): def reply_to_query(bot, update):
""" """
Handler for inline queries. Handler for inline queries.
Builds the result list for inline queries and answers to the client. Builds the result list for inline queries and answers to the client.
@ -647,13 +638,13 @@ def reply_to_query(update: Update, context: CallbackContext):
if players and game and len(players) > 1: if players and game and len(players) > 1:
switch = _('Current game: {game}').format(game=game.chat.title) switch = _('Current game: {game}').format(game=game.chat.title)
answer_async(context.bot, update.inline_query.id, results, cache_time=0, answer_async(bot, update.inline_query.id, results, cache_time=0,
switch_pm_text=switch, switch_pm_parameter='select') switch_pm_text=switch, switch_pm_parameter='select')
@game_locales @game_locales
@user_locale @user_locale
def process_result(update: Update, context: CallbackContext): def process_result(bot, update, job_queue):
""" """
Handler for chosen inline results. Handler for chosen inline results.
Checks the players actions and acts accordingly. Checks the players actions and acts accordingly.
@ -680,46 +671,38 @@ def process_result(update: Update, context: CallbackContext):
mode = result_id[5:] mode = result_id[5:]
game.set_mode(mode) game.set_mode(mode)
logger.info("Gamemode changed to {mode}".format(mode = mode)) logger.info("Gamemode changed to {mode}".format(mode = mode))
send_async(context.bot, chat.id, text=__("Gamemode changed to {mode}".format(mode = mode))) send_async(bot, chat.id, text=__("Gamemode changed to {mode}".format(mode = mode)))
return return
elif len(result_id) == 36: # UUID result elif len(result_id) == 36: # UUID result
return return
elif int(anti_cheat) != last_anti_cheat: elif int(anti_cheat) != last_anti_cheat:
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=__("Cheat attempt by {name}", multi=game.translate) text=__("Cheat attempt by {name}", multi=game.translate)
.format(name=display_name(player.user))) .format(name=display_name(player.user)))
return return
elif result_id == 'call_bluff': elif result_id == 'call_bluff':
reset_waiting_time(context.bot, player) reset_waiting_time(bot, player)
do_call_bluff(context.bot, player) do_call_bluff(bot, player)
elif result_id == 'draw': elif result_id == 'draw':
reset_waiting_time(context.bot, player) reset_waiting_time(bot, player)
do_draw(context.bot, player) do_draw(bot, player)
elif result_id == 'pass': elif result_id == 'pass':
game.turn() game.turn()
elif result_id in c.COLORS: elif result_id in c.COLORS:
game.choose_color(result_id) game.choose_color(result_id)
else: else:
reset_waiting_time(context.bot, player) reset_waiting_time(bot, player)
if game.mode == "text": do_play_card(bot, player, result_id)
sticker_id = c.STICKERS.get(result_id)
if sticker_id:
context.bot.sendSticker(chat.id,
sticker=sticker_id,
timeout=TIMEOUT)
else:
logger.warning(f"no sticker found for {result_id=}")
do_play_card(context.bot, player, result_id)
if game_is_running(game): if game_is_running(game):
nextplayer_message = ( nextplayer_message = (
__("Next player: {name}", multi=game.translate) __("Next player: {name}", multi=game.translate)
.format(name=display_name(game.current_player.user))) .format(name=display_name(game.current_player.user)))
choice = [[InlineKeyboardButton(text=_("Make your choice!"), switch_inline_query_current_chat='')]] choice = [[InlineKeyboardButton(text=_("Make your choice!"), switch_inline_query_current_chat='')]]
send_async(context.bot, chat.id, send_async(bot, chat.id,
text=nextplayer_message, text=nextplayer_message,
reply_markup=InlineKeyboardMarkup(choice)) reply_markup=InlineKeyboardMarkup(choice))
start_player_countdown(context.bot, game, context.job_queue) start_player_countdown(bot, game, job_queue)
def reset_waiting_time(bot, player): def reset_waiting_time(bot, player):

354
card.py
View file

@ -60,252 +60,122 @@ DRAW_FOUR = 'draw_four'
SPECIALS = (CHOOSE, DRAW_FOUR) SPECIALS = (CHOOSE, DRAW_FOUR)
CARDS_CLASSIC = {
"normal": {
"b_0": "BQADBAAD2QEAAl9XmQAB--inQsYcLTsC",
"b_1": "BQADBAAD2wEAAl9XmQABBzh4U-rFicEC",
"b_2": "BQADBAAD3QEAAl9XmQABo3l6TT0MzKwC",
"b_3": "BQADBAAD3wEAAl9XmQAB2y-3TSapRtIC",
"b_4": "BQADBAAD4QEAAl9XmQABT6nhOuolqKYC",
"b_5": "BQADBAAD4wEAAl9XmQABwRfmekGnpn0C",
"b_6": "BQADBAAD5QEAAl9XmQABQITgUsEsqxsC",
"b_7": "BQADBAAD5wEAAl9XmQABVhPF6EcfWjEC",
"b_8": "BQADBAAD6QEAAl9XmQABP6baig0pIvYC",
"b_9": "BQADBAAD6wEAAl9XmQAB0CQdsQs_pXIC",
"b_draw": "BQADBAAD7QEAAl9XmQAB00Wii7R3gDUC",
"b_skip": "BQADBAAD8QEAAl9XmQAB_RJHYKqlc-wC",
"b_reverse": "BQADBAAD7wEAAl9XmQABo7D0B9NUPmYC",
"g_0": "BQADBAAD9wEAAl9XmQABb8CaxxsQ-Y8C",
"g_1": "BQADBAAD-QEAAl9XmQAB9B6ti_j6UB0C",
"g_2": "BQADBAAD-wEAAl9XmQABYpLjOzbRz8EC",
"g_3": "BQADBAAD_QEAAl9XmQABKvc2ZCiY-D8C",
"g_4": "BQADBAAD_wEAAl9XmQABJB52wzPdHssC",
"g_5": "BQADBAADAQIAAl9XmQABp_Ep1I4GA2cC",
"g_6": "BQADBAADAwIAAl9XmQABaaMxxa4MihwC",
"g_7": "BQADBAADBQIAAl9XmQABv5Q264Crz8gC",
"g_8": "BQADBAADBwIAAl9XmQABjMH-X9UHh8sC",
"g_9": "BQADBAADCQIAAl9XmQAB26fZ2fW7vM0C",
"g_draw": "BQADBAADCwIAAl9XmQAB64jIZrgXrQUC",
"g_skip": "BQADBAADDwIAAl9XmQAB17yhhnh46VQC",
"g_reverse": "BQADBAADDQIAAl9XmQAB_xcaab0DkegC",
"r_0": "BQADBAADEQIAAl9XmQABiUfr1hz-zT8C",
"r_1": "BQADBAADEwIAAl9XmQAB5bWfwJGs6Q0C",
"r_2": "BQADBAADFQIAAl9XmQABHR4mg9Ifjw0C",
"r_3": "BQADBAADFwIAAl9XmQABYBx5O_PG2QIC",
"r_4": "BQADBAADGQIAAl9XmQABTQpGrlvet3cC",
"r_5": "BQADBAADGwIAAl9XmQABbdLt4gdntBQC",
"r_6": "BQADBAADHQIAAl9XmQABqEI274p3lSoC",
"r_7": "BQADBAADHwIAAl9XmQABCw8u67Q4EK4C",
"r_8": "BQADBAADIQIAAl9XmQAB8iDJmLxp8ogC",
"r_9": "BQADBAADIwIAAl9XmQAB_HCAww1kNGYC",
"r_draw": "BQADBAADJQIAAl9XmQABuz0OZ4l3k6MC",
"r_skip": "BQADBAADKQIAAl9XmQAC2AL5Ok_ULwI",
"r_reverse": "BQADBAADJwIAAl9XmQABu2tIeQTpDvUC",
"y_0": "BQADBAADKwIAAl9XmQAB_nWoNKe8DOQC",
"y_1": "BQADBAADLQIAAl9XmQABVprAGUDKgOQC",
"y_2": "BQADBAADLwIAAl9XmQABqyT4_YTm54EC",
"y_3": "BQADBAADMQIAAl9XmQABGC-Xxg_N6fIC",
"y_4": "BQADBAADMwIAAl9XmQABbc-ZGL8kApAC",
"y_5": "BQADBAADNQIAAl9XmQAB67QJZIF6XAcC",
"y_6": "BQADBAADNwIAAl9XmQABJg_7XXoITsoC",
"y_7": "BQADBAADOQIAAl9XmQABVrd7OcS2k34C",
"y_8": "BQADBAADOwIAAl9XmQABRpJSahBWk3EC",
"y_9": "BQADBAADPQIAAl9XmQAB9MwJWKLJogYC",
"y_draw": "BQADBAADPwIAAl9XmQABaPYK8oYg84cC",
"y_skip": "BQADBAADQwIAAl9XmQABO_AZKtxY6IMC",
"y_reverse": "BQADBAADQQIAAl9XmQABZdQFahGG6UQC",
"draw_four": "BQADBAAD9QEAAl9XmQABVlkSNfhn76cC",
"colorchooser": "BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C",
},
"not_playable": {
"b_0": "BQADBAADRQIAAl9XmQAB1IfkQ5xAiK4C",
"b_1": "BQADBAADRwIAAl9XmQABbWvhTeKBii4C",
"b_2": "BQADBAADSQIAAl9XmQABS1djHgyQokMC",
"b_3": "BQADBAADSwIAAl9XmQABwQ6VTbgY-MIC",
"b_4": "BQADBAADTQIAAl9XmQABAlKUYha8YccC",
"b_5": "BQADBAADTwIAAl9XmQABMvx8xVDnhUEC",
"b_6": "BQADBAADUQIAAl9XmQABDEbhP1Zd31kC",
"b_7": "BQADBAADUwIAAl9XmQABXb5XQBBaAnIC",
"b_8": "BQADBAADVQIAAl9XmQABgL5HRDLvrjgC",
"b_9": "BQADBAADVwIAAl9XmQABtO3XDQWZLtYC",
"b_draw": "BQADBAADWQIAAl9XmQAB2kk__6_2IhMC",
"b_skip": "BQADBAADXQIAAl9XmQABEGJI6CaH3vcC",
"b_reverse": "BQADBAADWwIAAl9XmQAB_kZA6UdHXU8C",
"g_0": "BQADBAADYwIAAl9XmQABGD5a9oG7Yg4C",
"g_1": "BQADBAADZQIAAl9XmQABqwABZHAXZIg0Ag",
"g_2": "BQADBAADZwIAAl9XmQABTI3mrEhojRkC",
"g_3": "BQADBAADaQIAAl9XmQABVi3rUyzWS3YC",
"g_4": "BQADBAADawIAAl9XmQABZIf5ThaXnpUC",
"g_5": "BQADBAADbQIAAl9XmQABNndVJSQCenIC",
"g_6": "BQADBAADbwIAAl9XmQABpoy1c4ZkrvwC",
"g_7": "BQADBAADcQIAAl9XmQABDeaT5fzxwREC",
"g_8": "BQADBAADcwIAAl9XmQABLIQ06ZM5NnAC",
"g_9": "BQADBAADdQIAAl9XmQABel-mC7eXGsMC",
"g_draw": "BQADBAADdwIAAl9XmQABOHEpxSztCf8C",
"g_skip": "BQADBAADewIAAl9XmQABDaQdMxjjPsoC",
"g_reverse": "BQADBAADeQIAAl9XmQABek1lGz7SJNAC",
"r_0": "BQADBAADfQIAAl9XmQABWrxoiXcsg0EC",
"r_1": "BQADBAADfwIAAl9XmQABlav-bkgSgRcC",
"r_2": "BQADBAADgQIAAl9XmQABDjZkqfJ4AdAC",
"r_3": "BQADBAADgwIAAl9XmQABT7lH7VVcy3MC",
"r_4": "BQADBAADhQIAAl9XmQAB1arPC5x0LrwC",
"r_5": "BQADBAADhwIAAl9XmQABWvs7xkCDldkC",
"r_6": "BQADBAADiQIAAl9XmQABjwABH5ZonWn8Ag",
"r_7": "BQADBAADiwIAAl9XmQABjekJfm4fBDIC",
"r_8": "BQADBAADjQIAAl9XmQABqFjchpsJeEkC",
"r_9": "BQADBAADjwIAAl9XmQAB-sKdcgABdNKDAg",
"r_draw": "BQADBAADkQIAAl9XmQABtw9RPVDHZOQC",
"r_skip": "BQADBAADlQIAAl9XmQABtG2GixCxtX4C",
"r_reverse": "BQADBAADkwIAAl9XmQABz2qyEbabnVsC",
"y_0": "BQADBAADlwIAAl9XmQABAb3ZwTGS1lMC",
"y_1": "BQADBAADmQIAAl9XmQAB9v5qJk9R0x8C",
"y_2": "BQADBAADmwIAAl9XmQABCsgpRHC2g-cC",
"y_3": "BQADBAADnQIAAl9XmQAB3kLLXCv-qY0C",
"y_4": "BQADBAADnwIAAl9XmQAB7R_y-NexNLIC",
"y_5": "BQADBAADoQIAAl9XmQABl-7mwsjD-cMC",
"y_6": "BQADBAADowIAAl9XmQABwbVsyv2MfPkC",
"y_7": "BQADBAADpQIAAl9XmQABoBqC0JsemVwC",
"y_8": "BQADBAADpwIAAl9XmQABpkwAAeh9ldlHAg",
"y_9": "BQADBAADqQIAAl9XmQABpSBEUfd4IM8C",
"y_draw": "BQADBAADqwIAAl9XmQABMt-2zW0VYb4C",
"y_skip": "BQADBAADrwIAAl9XmQABIDf-_TuuxtEC",
"y_reverse": "BQADBAADrQIAAl9XmQABm9M0Zh-_UwkC",
"draw_four": "BQADBAADYQIAAl9XmQAB_HWlvZIscDEC",
"colorchooser": "BQADBAADXwIAAl9XmQABY_ksDdMex-wC",
},
}
CARDS_CLASSIC_COLORBLIND = {
"normal": {
"colorchooser": "CAADBAADrg4AAvX2mVEpx_BiDIE5nQI",
"draw_four": "CAADBAADYRAAArnkmVGmqXHhjWEBxAI",
"r_0": "CAADBAAD6A8AAn_ckVHPWHqiBR_3jAI",
"r_1": "CAADBAAD5Q0AAg-ImVEx-blQI88RrQI",
"r_2": "CAADBAAD1g0AAuMjmVEkQsVhN49DMAI",
"r_3": "CAADBAADlhAAAqy4mVHWovoaWfQG_gI",
"r_4": "CAADBAADCRoAAqf_kVFnl8ACL1rjpwI",
"r_5": "CAADBAADVw8AAjmamVEEv2TVeL9cpQI",
"r_6": "CAADBAADHQ4AAuuUkVH2I-yn6nRBVAI",
"r_7": "CAADBAADNQ8AArP1kVF5rqHtk0pQ-AI",
"r_8": "CAADBAAD1BAAAuQDkVEPiIodUi6WvwI",
"r_9": "CAADBAAD2Q4AAq1nkFHM6z5C0Kff2QI",
"r_draw": "CAADBAADvQ8AAqZukFGEmkRSoSZQEwI",
"r_reverse": "CAADBAAD5RAAAg89mVE8-EY_2DifcAI",
"r_skip": "CAADBAADRg4AAp8bmVFOC6xdEZZRwwI",
"g_0": "CAADBAADTg4AAoQxmFF07jR_vfB4xgI",
"g_1": "CAADBAADQg4AAhkgmFGlsif9nNtXwgI",
"g_2": "CAADBAAD2BUAAue_mFGENiPSjZxbiQI",
"g_3": "CAADBAADpw4AAjO9mFHAOz8KD2n7BwI",
"g_4": "CAADBAADRhAAAqF7kFEcwLalLfDfaAI",
"g_5": "CAADBAADAg8AAqXLmFHJyg2F_ybbvwI",
"g_6": "CAADBAADVhYAAtK7mVGigRq_EkCuVgI",
"g_7": "CAADBAAD2RIAArccmFEj-8LIVNAbsgI",
"g_8": "CAADBAAD6AwAAuvmmFHBRarMimOWawI",
"g_9": "CAADBAADExEAAsNkmVFr8DaHGOwsggI",
"g_draw": "CAADBAADhA8AArxYmVH9ch5Jp00AAboC",
"g_reverse": "CAADBAADMhAAAvVOmFGH284LIY7cegI",
"g_skip": "CAADBAADbBcAAqinkVEOwkJtDRfk2gI",
"b_0": "CAADBAAD-BAAAkj8kFG61GJdw29QOAI",
"b_1": "CAADBAADcRMAAu-EmFFT1i4LcqO4OQI",
"b_2": "CAADBAAD0xQAAqVhmVHyrFSAbxtfjwI",
"b_3": "CAADBAADNg0AAn-xmFHev8IdF_ie0wI",
"b_4": "CAADBAADlQ4AAjZamVFcIL_pVB5cFwI",
"b_5": "CAADBAADrgwAAuL5mVHvEBZ8CG5p5QI",
"b_6": "CAADBAADDhUAAuGRmVGQYvmEOxczBAI",
"b_7": "CAADBAADIxEAAv_dmFEuVt39kkgZgwI",
"b_8": "CAADBAAD2w0AAoE6kVHG7WscV4F2hwI",
"b_9": "CAADBAADvQ0AArRMmVErWaSRP_giKQI",
"b_draw": "CAADBAADlw4AAjF_kFHPWSoYKBwtwQI",
"b_reverse": "CAADBAADog8AAqDJmVEJQp5WocnUnQI",
"b_skip": "CAADBAAD-QwAAgbZmFGltUlnslDNUQI",
"y_0": "CAADBAADrQ4AAr5WmVHNf69eBn2YOAI",
"y_1": "CAADBAADcg8AAmqKmVHfVeUI3u_i7AI",
"y_2": "CAADBAADkA4AAuDImFEQ8qjFlcKplQI",
"y_3": "CAADBAAD-QwAAmromFGAqVn-Y8N72wI",
"y_4": "CAADBAADjQ4AAmNLmFG80k7kfgx1NAI",
"y_5": "CAADBAADqQ8AAmgYmFH1_ey_bMQNYwI",
"y_6": "CAADBAADdQ0AAuWcmFEbG_gm1wGYCQI",
"y_7": "CAADBAAD6QwAApQAAZhRI8OfRvLX3vkC",
"y_8": "CAADBAADARAAAi-2kVEifJ-O9WVilgI",
"y_9": "CAADBAADxA0AAhQ8mFHjnl9tUCHSLAI",
"y_draw": "CAADBAADzw4AAncZmVEhLhX17eqX8AI",
"y_reverse": "CAADBAADTxAAAqgFmVEJRBw4eWgnDwI",
"y_skip": "CAADBAADPhYAAiGbkFG9hptFPLgj7wI",
},
"not_playable": {
"colorchooser": "CAADBAADpQ4AAlfDmFFHGkwyGFeCFQI",
"draw_four": "CAADBAADMRMAAv7amFHvKGLoNyFbNQI",
"r_0": "CAADBAADsBMAAuGdkFHTZ-jl4eNn-gI",
"r_1": "CAADBAADVA4AAhpfkFEKt19qveGSPgI",
"r_2": "CAADBAADrw0AAoWsmVHguULNoYJwUwI",
"r_3": "CAADBAADzxMAAjvkkFFdtKJu5WGwUgI",
"r_4": "CAADBAAD1Q8AAoHZkFFvyQnFHzfwiQI",
"r_5": "CAADBAADWxEAAvkHkFGUo86qxKV0kwI",
"r_6": "CAADBAAD_hIAAjx0mVGmlm-b_FHQBAI",
"r_7": "CAADBAADmhEAAslomFHOv7bqcDJkDAI",
"r_8": "CAADBAADtw0AAgqVmVG2HdSbcJYxZgI",
"r_9": "CAADBAADNxEAAuF6mVE3WzTMJkSVAgI",
"r_draw": "CAADBAADVxAAAiNukFE1K2xORNnfMwI",
"r_reverse": "CAADBAADQxMAAvH0mVHKznpt-uu9ngI",
"r_skip": "CAADBAADZA4AApbPkFFB9E2Px-HFpgI",
"g_0": "CAADBAAD8w4AAjDEmFG7DwKggUEj9QI",
"g_1": "CAADBAAD2g0AAo_DmVHIPG84WdIo1wI",
"g_2": "CAADBAADEhEAAoRXmVGIG2nuN45P6AI",
"g_3": "CAADBAADug8AAsSRmFFzk0TcRuG8VAI",
"g_4": "CAADBAADrQ8AAvgmkFESfo9BjF7-3gI",
"g_5": "CAADBAADVhAAAnPqkFFtxtFX9HlT-AI",
"g_6": "CAADBAADMg8AAiSBmFHIQw1jFjv6UwI",
"g_7": "CAADBAADvREAAv0BkVGDq3H1DCq_DQI",
"g_8": "CAADBAADWQ4AAhOEkVG96JDgCtFrEwI",
"g_9": "CAADBAAD2xYAAruDmFFAUMFryEwjoAI",
"g_draw": "CAADBAADLA4AAu9tkVGTzBbeeYydIQI",
"g_reverse": "CAADBAADVAwAAhYYmFExJS0ozE8-rAI",
"g_skip": "CAADBAADYg4AAulsmFHxOkaz9OsTiwI",
"b_0": "CAADBAADVxUAAtnOkFEIAAGw5CZEIxgC",
"b_1": "CAADBAAD1RAAAnQqkFF9kDqD0wp3ngI",
"b_2": "CAADBAADZg4AAvcUmVHTXwldirf1hAI",
"b_3": "CAADBAADfBAAAkX1mVHw0CWX0h31iQI",
"b_4": "CAADBAADPBAAAuTCmFFDpvXzes4qjwI",
"b_5": "CAADBAADTQ4AAsWQmVHcrxDQUWOB4AI",
"b_6": "CAADBAAD_hAAAoUhmVG8kjd65J8EngI",
"b_7": "CAADBAADlRAAArtjkFGko5TuFNnncwI",
"b_8": "CAADBAADZQ8AAltEmFE_fDYIXBrV3QI",
"b_9": "CAADBAADrhAAAtM-mVGwhrWTD9IaYgI",
"b_draw": "CAADBAADtQ0AAnVbmFGC1hI60JaOQQI",
"b_reverse": "CAADBAADShEAAlcOmFHStPeFzfVIEwI",
"b_skip": "CAADBAAD_xEAAgZFmVFMRA1J8Y1gxAI",
"y_0": "CAADBAAD7xAAAqjjmFHnCu7eKJvSBgI",
"y_1": "CAADBAADJQwAAp6tmFE2zDPVMieQ2QI",
"y_2": "CAADBAADNA4AAl2mmVFpQOxJ41gk_gI",
"y_3": "CAADBAAD3A4AAsxPmFGyZFv42UlxAQI",
"y_4": "CAADBAADwg8AAm88kVEc9HZpl2gmzQI",
"y_5": "CAADBAAD5hIAAkQ6mFHS-aGVuYZAnAI",
"y_6": "CAADBAADvQ8AAs3RmVHVkVBfEF7eIwI",
"y_7": "CAADBAAD1gwAAjlbmFGGH6rBdqP8QQI",
"y_8": "CAADBAADbg8AAqvXkVH1ESeZFcGVrgI",
"y_9": "CAADBAADOQ8AAnjokVG96pmCP7aZ3AI",
"y_draw": "CAADBAAD6w4AAgsJmVETUteFwqTVJgI",
"y_reverse": "CAADBAADtg8AAqiFmFFwothyN9TrXwI",
"y_skip": "CAADBAADSxEAAhcSmFGu_F5LffmsZgI",
},
}
STICKERS_OPTIONS = {
"option_draw": "BQADBAAD-AIAAl9XmQABxEjEcFM-VHIC",
"option_pass": "BQADBAAD-gIAAl9XmQABcEkAAbaZ4SicAg",
"option_bluff": "BQADBAADygIAAl9XmQABJoLfB9ntI2UC",
"option_info": "BQADBAADxAIAAl9XmQABC5v3Z77VLfEC",
}
# TODO: Support multiple card packs
# For now, just use classic colorblind
STICKERS = { STICKERS = {
**CARDS_CLASSIC_COLORBLIND["normal"], 'b_0': 'BQADBAAD2QEAAl9XmQAB--inQsYcLTsC',
**STICKERS_OPTIONS, 'b_1': 'BQADBAAD2wEAAl9XmQABBzh4U-rFicEC',
'b_2': 'BQADBAAD3QEAAl9XmQABo3l6TT0MzKwC',
'b_3': 'BQADBAAD3wEAAl9XmQAB2y-3TSapRtIC',
'b_4': 'BQADBAAD4QEAAl9XmQABT6nhOuolqKYC',
'b_5': 'BQADBAAD4wEAAl9XmQABwRfmekGnpn0C',
'b_6': 'BQADBAAD5QEAAl9XmQABQITgUsEsqxsC',
'b_7': 'BQADBAAD5wEAAl9XmQABVhPF6EcfWjEC',
'b_8': 'BQADBAAD6QEAAl9XmQABP6baig0pIvYC',
'b_9': 'BQADBAAD6wEAAl9XmQAB0CQdsQs_pXIC',
'b_draw': 'BQADBAAD7QEAAl9XmQAB00Wii7R3gDUC',
'b_skip': 'BQADBAAD8QEAAl9XmQAB_RJHYKqlc-wC',
'b_reverse': 'BQADBAAD7wEAAl9XmQABo7D0B9NUPmYC',
'g_0': 'BQADBAAD9wEAAl9XmQABb8CaxxsQ-Y8C',
'g_1': 'BQADBAAD-QEAAl9XmQAB9B6ti_j6UB0C',
'g_2': 'BQADBAAD-wEAAl9XmQABYpLjOzbRz8EC',
'g_3': 'BQADBAAD_QEAAl9XmQABKvc2ZCiY-D8C',
'g_4': 'BQADBAAD_wEAAl9XmQABJB52wzPdHssC',
'g_5': 'BQADBAADAQIAAl9XmQABp_Ep1I4GA2cC',
'g_6': 'BQADBAADAwIAAl9XmQABaaMxxa4MihwC',
'g_7': 'BQADBAADBQIAAl9XmQABv5Q264Crz8gC',
'g_8': 'BQADBAADBwIAAl9XmQABjMH-X9UHh8sC',
'g_9': 'BQADBAADCQIAAl9XmQAB26fZ2fW7vM0C',
'g_draw': 'BQADBAADCwIAAl9XmQAB64jIZrgXrQUC',
'g_skip': 'BQADBAADDwIAAl9XmQAB17yhhnh46VQC',
'g_reverse': 'BQADBAADDQIAAl9XmQAB_xcaab0DkegC',
'r_0': 'BQADBAADEQIAAl9XmQABiUfr1hz-zT8C',
'r_1': 'BQADBAADEwIAAl9XmQAB5bWfwJGs6Q0C',
'r_2': 'BQADBAADFQIAAl9XmQABHR4mg9Ifjw0C',
'r_3': 'BQADBAADFwIAAl9XmQABYBx5O_PG2QIC',
'r_4': 'BQADBAADGQIAAl9XmQABTQpGrlvet3cC',
'r_5': 'BQADBAADGwIAAl9XmQABbdLt4gdntBQC',
'r_6': 'BQADBAADHQIAAl9XmQABqEI274p3lSoC',
'r_7': 'BQADBAADHwIAAl9XmQABCw8u67Q4EK4C',
'r_8': 'BQADBAADIQIAAl9XmQAB8iDJmLxp8ogC',
'r_9': 'BQADBAADIwIAAl9XmQAB_HCAww1kNGYC',
'r_draw': 'BQADBAADJQIAAl9XmQABuz0OZ4l3k6MC',
'r_skip': 'BQADBAADKQIAAl9XmQAC2AL5Ok_ULwI',
'r_reverse': 'BQADBAADJwIAAl9XmQABu2tIeQTpDvUC',
'y_0': 'BQADBAADKwIAAl9XmQAB_nWoNKe8DOQC',
'y_1': 'BQADBAADLQIAAl9XmQABVprAGUDKgOQC',
'y_2': 'BQADBAADLwIAAl9XmQABqyT4_YTm54EC',
'y_3': 'BQADBAADMQIAAl9XmQABGC-Xxg_N6fIC',
'y_4': 'BQADBAADMwIAAl9XmQABbc-ZGL8kApAC',
'y_5': 'BQADBAADNQIAAl9XmQAB67QJZIF6XAcC',
'y_6': 'BQADBAADNwIAAl9XmQABJg_7XXoITsoC',
'y_7': 'BQADBAADOQIAAl9XmQABVrd7OcS2k34C',
'y_8': 'BQADBAADOwIAAl9XmQABRpJSahBWk3EC',
'y_9': 'BQADBAADPQIAAl9XmQAB9MwJWKLJogYC',
'y_draw': 'BQADBAADPwIAAl9XmQABaPYK8oYg84cC',
'y_skip': 'BQADBAADQwIAAl9XmQABO_AZKtxY6IMC',
'y_reverse': 'BQADBAADQQIAAl9XmQABZdQFahGG6UQC',
'draw_four': 'BQADBAAD9QEAAl9XmQABVlkSNfhn76cC',
'colorchooser': 'BQADBAAD8wEAAl9XmQABl9rUOPqx4E4C',
'option_draw': 'BQADBAAD-AIAAl9XmQABxEjEcFM-VHIC',
'option_pass': 'BQADBAAD-gIAAl9XmQABcEkAAbaZ4SicAg',
'option_bluff': 'BQADBAADygIAAl9XmQABJoLfB9ntI2UC',
'option_info': 'BQADBAADxAIAAl9XmQABC5v3Z77VLfEC'
} }
STICKERS_GREY = { STICKERS_GREY = {
**CARDS_CLASSIC_COLORBLIND["not_playable"], 'b_0': 'BQADBAADRQIAAl9XmQAB1IfkQ5xAiK4C',
'b_1': 'BQADBAADRwIAAl9XmQABbWvhTeKBii4C',
'b_2': 'BQADBAADSQIAAl9XmQABS1djHgyQokMC',
'b_3': 'BQADBAADSwIAAl9XmQABwQ6VTbgY-MIC',
'b_4': 'BQADBAADTQIAAl9XmQABAlKUYha8YccC',
'b_5': 'BQADBAADTwIAAl9XmQABMvx8xVDnhUEC',
'b_6': 'BQADBAADUQIAAl9XmQABDEbhP1Zd31kC',
'b_7': 'BQADBAADUwIAAl9XmQABXb5XQBBaAnIC',
'b_8': 'BQADBAADVQIAAl9XmQABgL5HRDLvrjgC',
'b_9': 'BQADBAADVwIAAl9XmQABtO3XDQWZLtYC',
'b_draw': 'BQADBAADWQIAAl9XmQAB2kk__6_2IhMC',
'b_skip': 'BQADBAADXQIAAl9XmQABEGJI6CaH3vcC',
'b_reverse': 'BQADBAADWwIAAl9XmQAB_kZA6UdHXU8C',
'g_0': 'BQADBAADYwIAAl9XmQABGD5a9oG7Yg4C',
'g_1': 'BQADBAADZQIAAl9XmQABqwABZHAXZIg0Ag',
'g_2': 'BQADBAADZwIAAl9XmQABTI3mrEhojRkC',
'g_3': 'BQADBAADaQIAAl9XmQABVi3rUyzWS3YC',
'g_4': 'BQADBAADawIAAl9XmQABZIf5ThaXnpUC',
'g_5': 'BQADBAADbQIAAl9XmQABNndVJSQCenIC',
'g_6': 'BQADBAADbwIAAl9XmQABpoy1c4ZkrvwC',
'g_7': 'BQADBAADcQIAAl9XmQABDeaT5fzxwREC',
'g_8': 'BQADBAADcwIAAl9XmQABLIQ06ZM5NnAC',
'g_9': 'BQADBAADdQIAAl9XmQABel-mC7eXGsMC',
'g_draw': 'BQADBAADdwIAAl9XmQABOHEpxSztCf8C',
'g_skip': 'BQADBAADewIAAl9XmQABDaQdMxjjPsoC',
'g_reverse': 'BQADBAADeQIAAl9XmQABek1lGz7SJNAC',
'r_0': 'BQADBAADfQIAAl9XmQABWrxoiXcsg0EC',
'r_1': 'BQADBAADfwIAAl9XmQABlav-bkgSgRcC',
'r_2': 'BQADBAADgQIAAl9XmQABDjZkqfJ4AdAC',
'r_3': 'BQADBAADgwIAAl9XmQABT7lH7VVcy3MC',
'r_4': 'BQADBAADhQIAAl9XmQAB1arPC5x0LrwC',
'r_5': 'BQADBAADhwIAAl9XmQABWvs7xkCDldkC',
'r_6': 'BQADBAADiQIAAl9XmQABjwABH5ZonWn8Ag',
'r_7': 'BQADBAADiwIAAl9XmQABjekJfm4fBDIC',
'r_8': 'BQADBAADjQIAAl9XmQABqFjchpsJeEkC',
'r_9': 'BQADBAADjwIAAl9XmQAB-sKdcgABdNKDAg',
'r_draw': 'BQADBAADkQIAAl9XmQABtw9RPVDHZOQC',
'r_skip': 'BQADBAADlQIAAl9XmQABtG2GixCxtX4C',
'r_reverse': 'BQADBAADkwIAAl9XmQABz2qyEbabnVsC',
'y_0': 'BQADBAADlwIAAl9XmQABAb3ZwTGS1lMC',
'y_1': 'BQADBAADmQIAAl9XmQAB9v5qJk9R0x8C',
'y_2': 'BQADBAADmwIAAl9XmQABCsgpRHC2g-cC',
'y_3': 'BQADBAADnQIAAl9XmQAB3kLLXCv-qY0C',
'y_4': 'BQADBAADnwIAAl9XmQAB7R_y-NexNLIC',
'y_5': 'BQADBAADoQIAAl9XmQABl-7mwsjD-cMC',
'y_6': 'BQADBAADowIAAl9XmQABwbVsyv2MfPkC',
'y_7': 'BQADBAADpQIAAl9XmQABoBqC0JsemVwC',
'y_8': 'BQADBAADpwIAAl9XmQABpkwAAeh9ldlHAg',
'y_9': 'BQADBAADqQIAAl9XmQABpSBEUfd4IM8C',
'y_draw': 'BQADBAADqwIAAl9XmQABMt-2zW0VYb4C',
'y_skip': 'BQADBAADrwIAAl9XmQABIDf-_TuuxtEC',
'y_reverse': 'BQADBAADrQIAAl9XmQABm9M0Zh-_UwkC',
'draw_four': 'BQADBAADYQIAAl9XmQAB_HWlvZIscDEC',
'colorchooser': 'BQADBAADXwIAAl9XmQABY_ksDdMex-wC'
} }

View file

@ -1,8 +0,0 @@
version: "3.9"
services:
unobot:
container_name: unobot
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped

View file

@ -58,7 +58,7 @@ class Game(object):
current_player = self.current_player current_player = self.current_player
itplayer = current_player.next itplayer = current_player.next
players.append(current_player) players.append(current_player)
while itplayer and itplayer != current_player: while itplayer and itplayer is not current_player:
players.append(itplayer) players.append(itplayer)
itplayer = itplayer.next itplayer = itplayer.next
return players return players
@ -121,7 +121,7 @@ class Game(object):
self.logger.debug("Draw counter increased by 2") self.logger.debug("Draw counter increased by 2")
elif card.value == c.REVERSE: elif card.value == c.REVERSE:
# Special rule for two players # Special rule for two players
if self.current_player == self.current_player.next.next: if self.current_player is self.current_player.next.next:
self.turn() self.turn()
else: else:
self.reverse() self.reverse()

View file

@ -110,7 +110,7 @@ class GameManager(object):
for g in games: for g in games:
for p in g.players: for p in g.players:
if p.user.id == user.id: if p.user.id == user.id:
if p == g.current_player: if p is g.current_player:
g.turn() g.turn()
p.leave() p.leave()

View file

@ -1,4 +0,0 @@
{
"api_id": 0,
"api_hash": ""
}

View file

@ -1,92 +0,0 @@
"""
Script to build the classic colorblind deck from the classic deck.
Requires imagemagick to be installed and in the path.
"""
from pathlib import Path
from shutil import copyfile
from subprocess import run
IMAGES_DIR = Path(__file__).resolve().parent
CLASSIC_DIR = IMAGES_DIR / "classic"
COLORBLIND_DIR = IMAGES_DIR / "classic_colorblind"
COLORBLIND_OVERLAY_DIR = IMAGES_DIR / "colorblind_overlay"
COLORS = ["r", "g", "b", "y"]
NUMBERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "draw", "reverse", "skip"]
SPECIALS = ["colorchooser", "draw_four"]
def overlay_image(color, number):
base = CLASSIC_DIR / "png" / f"{color}_{number}.png"
overlay = COLORBLIND_OVERLAY_DIR / f"{color}.png"
out = COLORBLIND_DIR / "png" / f"{color}_{number}.png"
run(["magick", "convert", str(base), str(overlay), "-composite", str(out)])
def create_not_playable(card):
base = COLORBLIND_DIR / "png" / f"{card}.png"
overlay = COLORBLIND_OVERLAY_DIR / "not_playable.png"
out = COLORBLIND_DIR / "png_not_playable" / f"{card}.png"
run(
[
"magick",
"convert",
str(base),
"-modulate",
"75,20",
"-brightness-contrast",
"0x10",
str(overlay),
"-composite",
str(out),
]
)
def convert_png_to_webp(suffix):
for color in COLORS:
for number in NUMBERS:
card = f"{color}_{number}"
png = COLORBLIND_DIR / f"png{suffix}" / f"{card}.png"
webp = COLORBLIND_DIR / f"webp{suffix}" / f"{card}.webp"
run(["magick", "convert", str(png), "-define", "webp:lossless=true", str(webp)])
for special in SPECIALS:
png = COLORBLIND_DIR / f"png{suffix}" / f"{special}.png"
webp = COLORBLIND_DIR / f"webp{suffix}" / f"{special}.webp"
run(["magick", "convert", str(png), "-define", "webp:lossless=true", str(webp)])
def main():
(COLORBLIND_DIR / "png").mkdir(parents=True, exist_ok=True)
(COLORBLIND_DIR / "png_not_playable").mkdir(parents=True, exist_ok=True)
(COLORBLIND_DIR / "webp").mkdir(parents=True, exist_ok=True)
(COLORBLIND_DIR / "webp_not_playable").mkdir(parents=True, exist_ok=True)
for color in COLORS:
for number in NUMBERS:
overlay_image(color, number)
for special in SPECIALS:
copyfile(
CLASSIC_DIR / "png" / f"{special}.png",
COLORBLIND_DIR / "png" / f"{special}.png",
)
for color in COLORS:
for number in NUMBERS:
create_not_playable(f"{color}_{number}")
for special in SPECIALS:
create_not_playable(special)
convert_png_to_webp("")
convert_png_to_webp("_not_playable")
if __name__ == "__main__":
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

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