Compare commits

...

27 commits

Author SHA1 Message Date
JerryXiao b9fd7efbcb
draw accumulation 2023-10-04 15:35:11 +08:00
JerryXiao fc51c39101
allow only one game perchat 2023-10-04 15:35:10 +08:00
Jannes Höke 7af19fae31 🐛 Add missing comma 2023-08-06 22:32:52 +02:00
Pradeep 09086f986a
🔀 Added Hindi Language (#118)
* Update available.py

* Update available.py

* Update compile.sh

* Add files via upload

* Update available.py

* Update compile.sh
2023-08-06 22:22:38 +02:00
Jannes Höke bda53c1b71
Merge pull request #116 from JuniorJPDJ/juniorjpdj/actions
feat(ci): build docker images on push and on the schedule
2023-06-11 12:23:42 +02:00
JuniorJPDJ f482af3330 feat(ci): build docker images on push and on the schedule 2023-06-10 18:12:59 +02:00
Jannes Höke 79c16e3920
Merge pull request #115 from JuniorJPDJ/fix-syntaxwarning-is
fix: SyntaxWarning: "is" with a literal. Did you mean "=="?
2023-06-07 14:01:39 +02:00
JuniorJPDJ 1e532ca6fc
fix: SyntaxWarning: "is" with a literal. Did you mean "=="? 2023-06-07 13:24:22 +02:00
Jannes Höke e25879cead 💄 Use new colorblind-accessable card stickers 2023-04-09 15:55:28 +02:00
Jannes Höke 0d936c961d 🍱 Add all colorblind images and helper scripts
also reorganize somes stuff
2023-04-09 15:30:05 +02:00
Jannes Höke 82471f5120 🔥 Delete JPG version of cards 2023-04-08 17:41:59 +02:00
Jannes Höke 68c04a9bd9 🚚 Move classic deck into subdirectory 2023-04-08 17:41:33 +02:00
Jannes Höke 034dd5ab34 📝 Credit Lê Minh Sơn for vi_VN translation 2023-03-05 15:18:20 +01:00
Jannes Höke 43e0479c6f 🌐 Fix plural form 2023-03-05 14:43:28 +01:00
Jannes Höke 6a467a9d0c 🌐 Fix translation 2023-03-05 14:39:59 +01:00
Jannes Höke 1785da37b5
🔀 Merge pull request #112 from leminhson06/master
Locale Việt Nam
2023-03-05 14:36:32 +01:00
leminhson06 56f623e321 Locale Việt Nam 2023-03-05 16:32:01 +07:00
Jannes Höke f16bf317b5 🐛 Use v13 signature for error handler 2022-12-07 14:06:58 +01:00
Jannes Höke 961a9ced76
🔀 Merge pull request #109 from Tackyou/master
Latest python in Dockerfile & add docker compose
2022-11-29 16:19:16 +01:00
tackyou ece86473a2 latest python in Dockerfile & add docker compose 2022-11-29 13:40:03 +01:00
Jannes Höke cb4d9bd5e3
🔀 Merge pull request #107 from tehcneko/master
Update python-telegram-bot to 13
2022-11-14 14:51:33 +01:00
NekoInverter c9e52174e1 Apply fixes from @jh0ker 2022-03-03 16:18:53 +08:00
tehcneko f11df72b0b Fix 2022-02-13 22:44:58 +08:00
tehcneko 1b7aea4320 Update python-telegram-bot to 13 2022-02-10 15:00:51 +08:00
Jannes Höke 4021c1483e
🔀 Merge pull request #106 from nikitastykov/master
Dockerfile changes :Changed the python version + removed extra requirements.txt
2021-12-10 15:46:39 +01:00
Nikita a8d5132dc5
Updated Dockerfile
Added the working and tested Python version, Previous imported the last version of python, which is not compatible
Removed extra install requirements.txt step
2021-12-09 05:46:44 +01:00
nikitastykov 6109c90d06 Changed the python version to the supported and tested one.
Removed extra requirements.txt installation.
2021-12-08 18:55:58 +00:00
644 changed files with 2430 additions and 330 deletions

60
.github/workflows/docker-publish.yml vendored Normal file
View file

@ -0,0 +1,60 @@
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,6 +27,8 @@ 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.
@ -69,3 +71,7 @@ target/
# Database file # Database file
uno.sqlite3 uno.sqlite3
images/api_auth.json
images/sticker_config.json
images/sticker_uploader.session

View file

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

150
Pipfile.lock generated
View file

@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "de56c4d5f516205e99d141cd7d372f67b602b6f981306971c01ffe25a5abf5c6" "sha256": "87f82f4abefdefd3b212fa99f5cbf6e222d6855aa7574d7a94fbf51b33cc342f"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.7" "python_version": "3.11"
}, },
"sources": [ "sources": [
{ {
@ -16,34 +16,150 @@
] ]
}, },
"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:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
"sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
], ],
"version": "==2019.6.16" "markers": "python_version >= '3.6'",
}, "version": "==2022.12.7"
"future": {
"hashes": [
"sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
],
"version": "==0.17.1"
}, },
"pony": { "pony": {
"hashes": [ "hashes": [
"sha256:55bb9d4d12029d8c2bbbc7a284970e72225035db7e6370c0a15ec93d1886fe88" "sha256:5f45fc67587f4520c560a57148cc573b097d42f82f5cb200d72c957b5708198d",
"sha256:608a1c1d662983bad2590e650f2bbc1cd6ed48558894ad8f50da4739ff98f614"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.7.10" "version": "==0.7.16"
}, },
"python-telegram-bot": { "python-telegram-bot": {
"hashes": [ "hashes": [
"sha256:238c4a88b09d93c52d413bcf7e7fe14dfeb02f5f9222ffe4cafd4bd3d55489a3", "sha256:534f5bb0ff4ca34c9252e97e0b3bcdab81d97be0eb4821682a361cb426c00e55",
"sha256:997983e5082dc6aa811bce3a6014731201fc64b0a9c02fdb26beac686029d94b" "sha256:baeff704baa2ac3dc17a944c02da888228ad258e89be2e5bcbd13a8a5102d573"
], ],
"index": "pypi", "index": "pypi",
"version": "==8.1.1" "version": "==13.11"
},
"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

@ -2,17 +2,18 @@
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 |
| id_ID | [Erwin Guo](https://www.facebook.com/erwinfransiscus) | | id_ID | [Erwin Guo](https://www.facebook.com/erwinfransiscus) |
| 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) |
| zh_CN | [imlonghao](https://github.com/imlonghao), [XhyEax](https://github.com/XhyEax) | | vi_VN | [Lê Minh Sơn](https://github.com/leminhson06) |
| zh_HK | [Jed Cheng](https://www.facebook.com/profile.php?id=100002258388821) | | zh_CN | [imlonghao](https://github.com/imlonghao), [XhyEax](https://github.com/XhyEax) |
| zh_TW | [Eugene Lam](https://www.facebook.com/eugenelam1118), [jimchen5209](https://www.youtube.com/user/jimchen5209), [pan93412](https://www.github.com/pan93412) | | zh_HK | [Jed Cheng](https://www.facebook.com/profile.php?id=100002258388821) |
| zh_TW | [Eugene Lam](https://www.facebook.com/eugenelam1118), [jimchen5209](https://www.youtube.com/user/jimchen5209), [pan93412](https://www.github.com/pan93412) |
Please add yourself here alphabetically when you submit your first translation. Please add yourself here alphabetically when you submit your first translation.

View file

@ -6,6 +6,8 @@ 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
@ -111,7 +113,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 is 0: if game.players_won == 0:
us.first_places += 1 us.first_places += 1
game.players_won += 1 game.players_won += 1
@ -153,11 +155,15 @@ 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 4 cards to {name}", text=__("Bluff called! Giving {count} cards to {name}" + draw_next_text,
multi=game.translate) multi=game.translate)
.format(name=player.prev.user.first_name)) .format(name=player.prev.user.first_name, count=draw_prev))
game.draw_counter = draw_prev
try: try:
player.prev.draw() player.prev.draw()
except DeckEmptyError: except DeckEmptyError:
@ -165,12 +171,21 @@ 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 6 cards to {name2}", text=__("{name1} didn't bluff! Giving {count} 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()
@ -191,7 +206,10 @@ def start_player_countdown(bot, game, job_queue):
if game.mode == 'fast': if game.mode == 'fast':
if game.job: if game.job:
game.job.schedule_removal() try:
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),
@ -205,9 +223,9 @@ def start_player_countdown(bot, game, job_queue):
player.game.job = job player.game.job = job
def skip_job(bot, job): def skip_job(context: CallbackContext):
player = job.context.player player = context.job.context.player
game = player.game game = player.game
if game_is_running(game): if game_is_running(game):
job_queue = job.context.job_queue job_queue = context.job.context.job_queue
do_skip(bot, player, job_queue) do_skip(context.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 InlineKeyboardButton, Update
from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \ from telegram.ext import InlineQueryHandler, ChosenInlineResultHandler, \
CommandHandler, MessageHandler, Filters, CallbackQueryHandler CommandHandler, MessageHandler, Filters, CallbackQueryHandler, CallbackContext
from telegram.ext.dispatcher import run_async from telegram.ext.dispatcher import run_async
import card as c import card as c
@ -49,9 +49,10 @@ 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(bot, update): def notify_me(update: Update, context: CallbackContext):
"""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':
@ -67,15 +68,25 @@ def notify_me(bot, update):
@user_locale @user_locale
def new_game(bot, update): def new_game(update: Update, context: CallbackContext):
"""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(bot, update) help_handler(update, context)
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,
@ -89,88 +100,88 @@ def new_game(bot, update):
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(bot, chat_id, send_async(context.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(bot, update): def kill_game(update: Update, context: CallbackContext):
"""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(bot, update) help_handler(update, context)
return return
if not games: if not games:
send_async(bot, chat.id, send_async(context.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, bot, chat): if user_is_creator_or_admin(user, game, context.bot, chat):
try: try:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate))
except NoGameInChatError: except NoGameInChatError:
send_async(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, update): def join_game(update: Update, context: CallbackContext):
"""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(bot, update) help_handler(update, context)
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(bot, chat.id, text=_("The lobby is closed")) send_async(context.bot, chat.id, text=_("The lobby is closed"))
except NoGameInChatError: except NoGameInChatError:
send_async(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, update): def leave_game(update: Update, context: CallbackContext):
"""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
@ -178,7 +189,7 @@ def leave_game(bot, update):
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(bot, chat.id, text=_("You are not playing in a game in " send_async(context.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
@ -190,23 +201,23 @@ def leave_game(bot, update):
gm.leave_game(user, chat) gm.leave_game(user, chat)
except NoGameInChatError: except NoGameInChatError:
send_async(bot, chat.id, text=_("You are not playing in a game in " send_async(context.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(bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate))
else: else:
if game.started: if game.started:
send_async(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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)),
@ -214,11 +225,11 @@ def leave_game(bot, update):
@user_locale @user_locale
def kick_player(bot, update): def kick_player(update: Update, context: CallbackContext):
"""Handler for the /kick command""" """Handler for the /kick command"""
if update.message.chat.type == 'private': if update.message.chat.type == 'private':
help_handler(bot, update) help_handler(update, context)
return return
chat = update.message.chat chat = update.message.chat
@ -228,20 +239,20 @@ def kick_player(bot, update):
game = gm.chatid_games[chat.id][-1] game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
send_async(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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, bot, chat): if user_is_creator_or_admin(user, game, context.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
@ -250,40 +261,40 @@ def kick_player(bot, update):
gm.leave_game(kicked, chat) gm.leave_game(kicked, chat)
except NoGameInChatError: except NoGameInChatError:
send_async(bot, chat.id, text=_("Player {name} is not found in the current game.".format(name=display_name(kicked))), send_async(context.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(bot, chat.id, send_async(context.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(bot, chat.id, text=__("Game ended!", multi=game.translate)) send_async(context.bot, chat.id, text=__("Game ended!", multi=game.translate))
return return
send_async(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, chat.id, send_async(context.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(bot, update): def select_game(update: Update, context: CallbackContext):
"""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)
@ -299,16 +310,15 @@ def select_game(bot, update):
text=_("Game not found.")) text=_("Game not found."))
return return
@run_async def selected():
def selected(bot):
back = [[InlineKeyboardButton(text=_("Back to last group"), back = [[InlineKeyboardButton(text=_("Back to last group"),
switch_inline_query='')]] switch_inline_query='')]]
bot.answerCallbackQuery(update.callback_query.id, context.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)
bot.editMessageText(chat_id=update.callback_query.message.chat_id, context.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 "
@ -318,11 +328,11 @@ def select_game(bot, update):
parse_mode=ParseMode.HTML, parse_mode=ParseMode.HTML,
timeout=TIMEOUT) timeout=TIMEOUT)
selected(bot) dispatcher.run_async(selected)
@game_locales @game_locales
def status_update(bot, update): def status_update(update: Update, context: CallbackContext):
"""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
@ -337,17 +347,17 @@ def status_update(bot, update):
pass pass
except NotEnoughPlayersError: except NotEnoughPlayersError:
gm.end_game(chat, user) gm.end_game(chat, user)
send_async(bot, chat.id, text=__("Game ended!", send_async(context.bot, chat.id, text=__("Game ended!",
multi=game.translate)) multi=game.translate))
else: else:
send_async(bot, chat.id, text=__("Removing {name} from the game", send_async(context.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(bot, update, args, job_queue): def start_game(update: Update, context: CallbackContext):
"""Handler for the /start command""" """Handler for the /start command"""
if update.message.chat.type != 'private': if update.message.chat.type != 'private':
@ -356,16 +366,16 @@ def start_game(bot, update, args, job_queue):
try: try:
game = gm.chatid_games[chat.id][-1] game = gm.chatid_games[chat.id][-1]
except (KeyError, IndexError): except (KeyError, IndexError):
send_async(bot, chat.id, send_async(context.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(bot, chat.id, text=_("The game has already started")) send_async(context.bot, chat.id, text=_("The game has already started"))
elif len(game.players) < MIN_PLAYERS: elif len(game.players) < MIN_PLAYERS:
send_async(bot, chat.id, send_async(context.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))
@ -383,30 +393,29 @@ def start_game(bot, update, args, job_queue):
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"""
bot.sendSticker(chat.id, context.bot.sendSticker(chat.id,
sticker=c.STICKERS[str(game.last_card)], sticker=c.STICKERS[str(game.last_card)],
timeout=TIMEOUT) timeout=TIMEOUT)
bot.sendMessage(chat.id, context.bot.sendMessage(chat.id,
text=first_message, text=first_message,
reply_markup=InlineKeyboardMarkup(choice), reply_markup=InlineKeyboardMarkup(choice),
timeout=TIMEOUT) timeout=TIMEOUT)
send_first() dispatcher.run_async(send_first)
start_player_countdown(bot, game, job_queue) start_player_countdown(context.bot, game, context.job_queue)
elif len(args) and args[0] == 'select': elif len(context.args) and context.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 is gm.userid_current[update.message.from_user.id]: if player == gm.userid_current[update.message.from_user.id]:
title = '- %s -' % player.game.chat.title title = '- %s -' % player.game.chat.title
groups.append( groups.append(
@ -414,23 +423,23 @@ def start_game(bot, update, args, job_queue):
callback_data=str(player.game.chat.id))] callback_data=str(player.game.chat.id))]
) )
send_async(bot, update.message.chat_id, send_async(context.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(bot, update) help_handler(update, context)
@user_locale @user_locale
def close_game(bot, update): def close_game(update: Update, context: CallbackContext):
"""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(bot, chat.id, send_async(context.bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -438,12 +447,12 @@ def close_game(bot, update):
if user.id in game.owner: if user.id in game.owner:
game.open = False game.open = False
send_async(bot, chat.id, text=_("Closed the lobby. " send_async(context.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(bot, chat.id, send_async(context.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)
@ -451,14 +460,14 @@ def close_game(bot, update):
@user_locale @user_locale
def open_game(bot, update): def open_game(update: Update, context: CallbackContext):
"""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(bot, chat.id, send_async(context.bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -466,11 +475,11 @@ def open_game(bot, update):
if user.id in game.owner: if user.id in game.owner:
game.open = True game.open = True
send_async(bot, chat.id, text=_("Opened the lobby. " send_async(context.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(bot, chat.id, send_async(context.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)
@ -478,14 +487,14 @@ def open_game(bot, update):
@user_locale @user_locale
def enable_translations(bot, update): def enable_translations(update: Update, context: CallbackContext):
"""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(bot, chat.id, send_async(context.bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -493,12 +502,12 @@ def enable_translations(bot, update):
if user.id in game.owner: if user.id in game.owner:
game.translate = True game.translate = True
send_async(bot, chat.id, text=_("Enabled multi-translations. " send_async(context.bot, chat.id, text=_("Enabled multi-translations. "
"Disable with /disable_translations")) "Disable with /disable_translations"))
return return
else: else:
send_async(bot, chat.id, send_async(context.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)
@ -506,14 +515,14 @@ def enable_translations(bot, update):
@user_locale @user_locale
def disable_translations(bot, update): def disable_translations(update: Update, context: CallbackContext):
"""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(bot, chat.id, send_async(context.bot, chat.id,
text=_("There is no running game in this chat.")) text=_("There is no running game in this chat."))
return return
@ -521,13 +530,13 @@ def disable_translations(bot, update):
if user.id in game.owner: if user.id in game.owner:
game.translate = False game.translate = False
send_async(bot, chat.id, text=_("Disabled multi-translations. " send_async(context.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(bot, chat.id, send_async(context.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)
@ -536,14 +545,14 @@ def disable_translations(bot, update):
@game_locales @game_locales
@user_locale @user_locale
def skip_player(bot, update): def skip_player(update: Update, context: CallbackContext):
"""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(bot, chat.id, send_async(context.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
@ -558,19 +567,19 @@ def skip_player(bot, update):
# 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(bot, chat.id, send_async(context.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(bot, player) do_skip(context.bot, player)
@game_locales @game_locales
@user_locale @user_locale
def reply_to_query(bot, update): def reply_to_query(update: Update, context: CallbackContext):
""" """
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.
@ -638,13 +647,13 @@ def reply_to_query(bot, update):
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(bot, update.inline_query.id, results, cache_time=0, answer_async(context.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(bot, update, job_queue): def process_result(update: Update, context: CallbackContext):
""" """
Handler for chosen inline results. Handler for chosen inline results.
Checks the players actions and acts accordingly. Checks the players actions and acts accordingly.
@ -671,38 +680,46 @@ def process_result(bot, update, job_queue):
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(bot, chat.id, text=__("Gamemode changed to {mode}".format(mode = mode))) send_async(context.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(bot, chat.id, send_async(context.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(bot, player) reset_waiting_time(context.bot, player)
do_call_bluff(bot, player) do_call_bluff(context.bot, player)
elif result_id == 'draw': elif result_id == 'draw':
reset_waiting_time(bot, player) reset_waiting_time(context.bot, player)
do_draw(bot, player) do_draw(context.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(bot, player) reset_waiting_time(context.bot, player)
do_play_card(bot, player, result_id) if game.mode == "text":
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(bot, chat.id, send_async(context.bot, chat.id,
text=nextplayer_message, text=nextplayer_message,
reply_markup=InlineKeyboardMarkup(choice)) reply_markup=InlineKeyboardMarkup(choice))
start_player_countdown(bot, game, job_queue) start_player_countdown(context.bot, game, context.job_queue)
def reset_waiting_time(bot, player): def reset_waiting_time(bot, player):

354
card.py
View file

@ -60,122 +60,252 @@ 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 = {
'b_0': 'BQADBAAD2QEAAl9XmQAB--inQsYcLTsC', **CARDS_CLASSIC_COLORBLIND["normal"],
'b_1': 'BQADBAAD2wEAAl9XmQABBzh4U-rFicEC', **STICKERS_OPTIONS,
'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 = {
'b_0': 'BQADBAADRQIAAl9XmQAB1IfkQ5xAiK4C', **CARDS_CLASSIC_COLORBLIND["not_playable"],
'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'
} }

8
docker-compose.yml Normal file
View file

@ -0,0 +1,8 @@
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 is not current_player: while itplayer and itplayer != 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 is self.current_player.next.next: if self.current_player == 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 is g.current_player: if p == g.current_player:
g.turn() g.turn()
p.leave() p.leave()

View file

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

View file

@ -0,0 +1,92 @@
"""
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()

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

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