diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
new file mode 100644
index 0000000..bbbb2de
--- /dev/null
+++ b/.github/workflows/unit-tests.yaml
@@ -0,0 +1,27 @@
+name: Python Unit Tests
+
+on: [push, pull_request] # Define when the workflow should be triggered
+
+jobs:
+ test:
+ name: Run Unit Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.11'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Run unittest
+ run: |
+ echo '{}' > config.json # Create an empty config file
+ python -m unittest
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..227c0a0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+config.py
+*.session*
+*database.sqlite*
+__pycache__/
+*/__pycache__/
+config.py
diff --git a/bot.py b/bot.py
new file mode 100644
index 0000000..3b1d8ee
--- /dev/null
+++ b/bot.py
@@ -0,0 +1,18 @@
+from config import bot
+from hydrogram import idle
+from tortoise import run_async
+from version import ascii_art
+from db import connect_database
+import os
+
+
+async def main():
+ os.system("clear")
+ print(ascii_art)
+ await connect_database()
+ await bot.start()
+ await idle()
+ await bot.stop()
+
+
+run_async(main())
diff --git a/card.py b/card.py
new file mode 100644
index 0000000..407d48a
--- /dev/null
+++ b/card.py
@@ -0,0 +1,56 @@
+import os
+import json
+
+RED = "r"
+BLUE = "b"
+GREEN = "g"
+YELLOW = "y"
+BLACK = "x"
+
+COLORS = (RED, BLUE, GREEN, YELLOW)
+
+COLOR_ICONS = {RED: "❤️", BLUE: "💙", GREEN: "💚", YELLOW: "💛", BLACK: "⬛️"}
+
+# Values
+ZERO = "0"
+ONE = "1"
+TWO = "2"
+THREE = "3"
+FOUR = "4"
+FIVE = "5"
+SIX = "6"
+SEVEN = "7"
+EIGHT = "8"
+NINE = "9"
+DRAW_TWO = "draw"
+REVERSE = "reverse"
+SKIP = "skip"
+
+VALUES = (
+ ZERO,
+ ONE,
+ TWO,
+ THREE,
+ FOUR,
+ FIVE,
+ SIX,
+ SEVEN,
+ EIGHT,
+ NINE,
+ DRAW_TWO,
+ REVERSE,
+ SKIP,
+)
+
+# Special cards
+CHOOSE = "colorchooser"
+DRAW_FOUR = "draw_four"
+
+SPECIALS = (CHOOSE, DRAW_FOUR)
+
+cards = {}
+for filename in os.listdir("cards"):
+ if filename.endswith(".json"):
+ name = filename.split(".")[0]
+ with open(f"cards/{filename}", "r") as f:
+ cards[name] = json.load(f)
diff --git a/cards/classic.json b/cards/classic.json
new file mode 100644
index 0000000..b01c2ac
--- /dev/null
+++ b/cards/classic.json
@@ -0,0 +1,152 @@
+{
+ "STICKERS": {
+ "b_0": "CAACAgQAAxkDAAI372NtY-V641fF6HhAA4Vuc6CbI_LeAALZAQACX1eZAAEqnpNt3SpG_ysE",
+ "b_1": "CAACAgQAAxkDAAI38GNtY-UvkNQN3h5p5n_dfNbhPV9HAALbAQACX1eZAAHluPl_BVzaDisE",
+ "b_2": "CAACAgQAAxkDAAI38WNtY-X4Gvnxt4mofZ-Uv_zmGWHRAALdAQACX1eZAAEFe5JBdpP-cysE",
+ "b_3": "CAACAgQAAxkDAAI38mNtY-av7Gm6hUEdRs_mONWGzKoGAALfAQACX1eZAAFQJXWHQ2D7uisE",
+ "b_4": "CAACAgQAAxkDAAI382NtY-YXxHbN1MfXSl6FbzwgWq5vAALhAQACX1eZAAHo1SP4devY_ysE",
+ "b_5": "CAACAgQAAxkDAAI39GNtY-dNEOn0i1luuPjPOHvqyasxAALjAQACX1eZAALf6g-FruzaKwQ",
+ "b_6": "CAACAgQAAxkDAAI39WNtY-df9ew41xXE6ARS3VHDKg0NAALlAQACX1eZAAHwMoU1Nb4OgisE",
+ "b_7": "CAACAgQAAxkDAAI39mNtY-ftENXWBUBNqNTomh-NeufNAALnAQACX1eZAAFOBAnoop1fWisE",
+ "b_8": "CAACAgQAAxkDAAI392NtY-idvjst_LSKlwP2cEDnS3WpAALpAQACX1eZAAHmKrizqjwJ3isE",
+ "b_9": "CAACAgQAAxkDAAI3-GNtY-jEw-hh0ei6OxSl2r4DehmIAALrAQACX1eZAAHvul-ZztVWiisE",
+ "b_draw": "CAACAgQAAxkDAAI3-WNtY-nrtJj_c48YtjbPwydARdwJAALtAQACX1eZAAGdURg9n6qvEysE",
+ "b_skip": "CAACAgQAAxkDAAI3-mNtY-kVI0dIVd38sOvZrZmtRCv_AALxAQACX1eZAAHAf0ks_Y82JysE",
+ "b_reverse": "CAACAgQAAxkDAAI3-2NtY-p4_EUUTVDYKX12SMcKA9IbAALvAQACX1eZAAFjAZc535XzNSsE",
+ "g_0": "CAACAgQAAxkDAAI3_GNtY-qvO3V8NwHojOpf8aIpbnYvAAL3AQACX1eZAAH7m-CsNWDzBSsE",
+ "g_1": "CAACAgQAAxkDAAI3_WNtY-r28bGOeJGKL7ZtEwUrWXzfAAL5AQACX1eZAAFVNSG--aqs9CsE",
+ "g_2": "CAACAgQAAxkDAAI3_mNtY-sjqfdB5nu7iKPFqHRItFerAAL7AQACX1eZAAHDX5Qn7VbSdCsE",
+ "g_3": "CAACAgQAAxkDAAI3_2NtY-ueCVLB_KL8Xz0itFJGWNbYAAL9AQACX1eZAAGwUxSSKSNPaisE",
+ "g_4": "CAACAgQAAxkDAAI4AAFjbWPsRRrrb0KdkF5SGCO87ni9sAAC_wEAAl9XmQABARICqk9L7OArBA",
+ "g_5": "CAACAgQAAxkDAAI4AWNtY-zlRyWdS69Z4bcwBgklRcBEAAIBAgACX1eZAAGN2wN5nVhf3ysE",
+ "g_6": "CAACAgQAAxkDAAI4AmNtY-zXK3F2NTz-XaFeDk2rsP7NAAIDAgACX1eZAAFaJA80kw1XfSsE",
+ "g_7": "CAACAgQAAxkDAAI4A2NtY-0dqOmBW9-XK_BbtXg0OLRaAAIFAgACX1eZAAGDbLTCiNGLBisE",
+ "g_8": "CAACAgQAAxkDAAI4BGNtY-2kF7oUCmvU_AbU9lmudtZqAAIHAgACX1eZAAGnWrRTRZj7gSsE",
+ "g_9": "CAACAgQAAxkDAAI4BWNtY-60BpqwiJQ8-p93unknqHi2AAIJAgACX1eZAAHODOPdhwzltysE",
+ "g_draw": "CAACAgQAAxkDAAI4BmNtY-53H6EJgbUQSeEpguubOevXAAILAgACX1eZAAFWg06uGplHVysE",
+ "g_skip": "CAACAgQAAxkDAAI4B2NtY-5VirooDDZAWu4ENrVBBoFHAAIPAgACX1eZAAHn-hBXxRvYQisE",
+ "g_reverse": "CAACAgQAAxkDAAI4CGNtY-_G8b0fBt0N3OBgx9CIwJziAAINAgACX1eZAAFMYqmCS3vfySsE",
+ "r_0": "CAACAgQAAxkDAAI4CWNtY-9LOKHb1FqCn3GmqxOYCo_fAAIRAgACX1eZAAHK9atgT_cu_isE",
+ "r_1": "CAACAgQAAxkDAAI4CmNtY_Dxb_ivl-VHFRDPgHVOilCVAAITAgACX1eZAAH_6pt2airFESsE",
+ "r_2": "CAACAgQAAxkDAAI4C2NtY_B9bP3cd73NvBd-Un8yZTYzAAIVAgACX1eZAAHQrmSSeMDfgCsE",
+ "r_3": "CAACAgQAAxkDAAI4DGNtY_Hqk5RPHjNn50jy_ImBPYZLAAIXAgACX1eZAAFeHWWPa-piRysE",
+ "r_4": "CAACAgQAAxkDAAI4DWNtY_HqY4wNkPulTWHIY9d2Fep-AAIZAgACX1eZAAE7VUWywkd3KCsE",
+ "r_5": "CAACAgQAAxkDAAI4DmNtY_Lb9j5Qi5RVPEaSW3uZWAnlAAIbAgACX1eZAAF1s0b9V-PUJCsE",
+ "r_6": "CAACAgQAAxkDAAI4D2NtY_Kklm1t7E0KShmWTbXEwnpNAAIdAgACX1eZAAF8hSz11exIUisE",
+ "r_7": "CAACAgQAAxkDAAI4EGNtY_LoR07j-LayjpoVlEPLCCe0AAIfAgACX1eZAAEVnCo1RKSqnCsE",
+ "r_8": "CAACAgQAAxkDAAI4EWNtY_OrIOu5PPIUTZ-cn0FBFcT2AAIhAgACX1eZAAEhXezQrbzKOisE",
+ "r_9": "CAACAgQAAxkDAAI4EmNtY_PI6uILsPHkkyIDFp4ivFBJAAIjAgACX1eZAAHN4GBkUaxpqisE",
+ "r_draw": "CAACAgQAAxkDAAI4E2NtY_SNrUaYiRbAIEi9c_X-veafAAIlAgACX1eZAAGZvG1zNp2cVisE",
+ "r_skip": "CAACAgQAAxkDAAI4FGNtY_SrNSCK9k9FO9Xji2fb9LJMAAIpAgACX1eZAAFprUDwYHBu3SsE",
+ "r_reverse": "CAACAgQAAxkDAAI4FWNtY_V41t8UX4XtxugfwVMibbqLAAInAgACX1eZAAGay7EvXnoVZisE",
+ "y_0": "CAACAgQAAxkDAAI4FmNtY_XYaAevT9wxGiAxI1n6e_spAAIrAgACX1eZAAG1mgAB2D5sIc8rBA",
+ "y_1": "CAACAgQAAxkDAAI4F2NtY_aD1zsrQYWtYoeePhDN1bcvAAItAgACX1eZAAHqNCCjuSEQjisE",
+ "y_2": "CAACAgQAAxkDAAI4GGNtY_Y9kN6nzxvk8KwX8SnwTntmAAIvAgACX1eZAAH4u547rBAiBCsE",
+ "y_3": "CAACAgQAAxkDAAI4GWNtY_dJwM67rmUFcLEtByedoFJdAAIxAgACX1eZAAFBQ00TMrpMeisE",
+ "y_4": "CAACAgQAAxkDAAI4GmNtY_d6JUufI61BWnqI4DTVRxMVAAIzAgACX1eZAAF7IOqIuGqyDSsE",
+ "y_5": "CAACAgQAAxkDAAI4G2NtY_dxij19aBCA7Tjf5ytWzXgNAAI1AgACX1eZAAHyIiYzI-E-LisE",
+ "y_6": "CAACAgQAAxkDAAI4HGNtY_hPQ2iuWWmADOUYR-P-nNVFAAI3AgACX1eZAAH_E8fuZ374hysE",
+ "y_7": "CAACAgQAAxkDAAI4HWNtY_jp9tXZ3lpAV83tzDcazcA4AAI5AgACX1eZAAHPK6qSI6Ku_CsE",
+ "y_8": "CAACAgQAAxkDAAI4HmNtY_kQwEGUW6F38bBIYXfspzarAAI7AgACX1eZAAHXiL4XwJi0eysE",
+ "y_9": "CAACAgQAAxkDAAI4H2NtY_kJ_ofl80XkaVobKpd-IgqQAAI9AgACX1eZAAGG_opl6vQSOCsE",
+ "y_draw": "CAACAgQAAxkDAAI4IGNtY_qbKj2mnuJVlTai4F6se8MNAAI_AgACX1eZAAFrjyuhcA2ksysE",
+ "y_skip": "CAACAgQAAxkDAAI4IWNtY_rKy-RTeKjfZT0RAYNNreVhAAJDAgACX1eZAAF1m63alvMoxysE",
+ "y_reverse": "CAACAgQAAxkDAAI4ImNtY_vaX0rQZ_5ZUeFTpMa2ZQABOwACQQIAAl9XmQABCHpDm7MPbakrBA",
+ "draw_four": "CAACAgQAAxkDAAI4I2NtY_vr1Fa4_Q2Y6dxOopNX7sSsAAL1AQACX1eZAAHXOgABZUCgVkkrBA",
+ "colorchooser": "CAACAgQAAxkDAAI4JGNtY_vpncCbuHH2xDLokQWxUAXSAALzAQACX1eZAAHI5jbpFQE9bCsE",
+ "option_draw": "CAACAgQAAxkDAAI4JWNtY_zry4NT2JAlWjTryYiuec4nAAL4AgACX1eZAAH-TdXSlvEa2ysE",
+ "option_pass": "CAACAgQAAxkDAAI4JmNtY_yMlr6rB3UdTikR3zFCk8kVAAL6AgACX1eZAAFuilR5QnD-VysE",
+ "option_bluff": "CAACAgQAAxkDAAI4J2NtY_2Dmt5Mi4iZhsUh32OeNVe7AALKAgACX1eZAAHBw478rNqN0CsE",
+ "option_info": "CAACAgQAAxkDAAI4KGNtY_3tO0Sxhu5NzF1UA3tdUnklAALEAgACX1eZAAGi2Qy93IIQwisE"
+ },
+ "STICKERS_GREY": {
+ "b_0": "CAACAgQAAxkDAAI4KWNtY_3SM2AGtecbGE8XDjlWvcKxAAJFAgACX1eZAAHwXYFNZhQaIysE",
+ "b_1": "CAACAgQAAxkDAAI4KmNtY_7zNsvijvvGZAJmuxcYVgizAAJHAgACX1eZAAF_ZxC64wgdNCsE",
+ "b_2": "CAACAgQAAxkDAAI4K2NtY_4z7XEHPzcliqJth5G3ds6vAAJJAgACX1eZAAF-GuNgJ25IAAErBA",
+ "b_3": "CAACAgQAAxkDAAI4LGNtY_9ZPE9nPCPJQ0Rjf_zOkTsiAAJLAgACX1eZAAHIJQ71XJ39mCsE",
+ "b_4": "CAACAgQAAxkDAAI4LWNtY_--OWOFczobsp10PPj5p9pZAAJNAgACX1eZAAEjmR2mhJ8SsSsE",
+ "b_5": "CAACAgQAAxkDAAI4LmNtZAABTkAAAT7kcgxZkdA3rcZmxM0AAk8CAAJfV5kAASN8DC8z_yexKwQ",
+ "b_6": "CAACAgQAAxkDAAI4L2NtZAABOSkvi7YF9opHBHILrQukJwACUQIAAl9XmQABv35eqFpp188rBA",
+ "b_7": "CAACAgQAAxkDAAI4MGNtZAABcb94kfODfzBiW7R6caIITgACUwIAAl9XmQABv8VaivrtncwrBA",
+ "b_8": "CAACAgQAAxkDAAI4MWNtZAEPZcxI8yZZJ7mtvLEhRyQyAAJVAgACX1eZAAF8hUb4bS_NdCsE",
+ "b_9": "CAACAgQAAxkDAAI4MmNtZAHG55HKa6LNKc496jAPrUCzAAJXAgACX1eZAAGXAmJ0BKvi1ysE",
+ "b_draw": "CAACAgQAAxkDAAI4M2NtZALeN87Xgly5X7j5XK0dfaznAAJZAgACX1eZAAFS-DsDXK7zdisE",
+ "b_skip": "CAACAgQAAxkDAAI4NGNtZAJR4ZxfKgABx3HNLp-9w8fNagACXQIAAl9XmQABc7AYk0bGSHorBA",
+ "b_reverse": "CAACAgQAAxkDAAI4NWNtZAKP0DU5ZIh-4eID9fwEqWDhAAJbAgACX1eZAAHRLf8w4EEJfysE",
+ "g_0": "CAACAgQAAxkDAAI4NmNtZAMTsoTxk-Gzg61XUbgiWmuDAAJjAgACX1eZAAG_c8FzjSBlOCsE",
+ "g_1": "CAACAgQAAxkDAAI4N2NtZAOMsFWlo1a6VbET_L4Z33qjAAJlAgACX1eZAAH2R3CHmHduZCsE",
+ "g_2": "CAACAgQAAxkDAAI4OGNtZASmomincPijzQaGuhzS4NT3AAJnAgACX1eZAAHB14u8vZ5pjSsE",
+ "g_3": "CAACAgQAAxkDAAI4OWNtZATXrH2F0kmklBKkx5-yLbqeAAJpAgACX1eZAAFaZGnJmMcN9CsE",
+ "g_4": "CAACAgQAAxkDAAI4OmNtZARrtuTkDtrmFwSWGCMNNyzVAAJrAgACX1eZAAF3KxLEqQq8KysE",
+ "g_5": "CAACAgQAAxkDAAI4O2NtZAXsq9mIqylmXkuqblUSZ_s5AAJtAgACX1eZAAGObwogvTEInCsE",
+ "g_6": "CAACAgQAAxkDAAI4PGNtZAXYyNLL6UnAXV2J5fcYDSjcAAJvAgACX1eZAAEpOGFMRnLGmSsE",
+ "g_7": "CAACAgQAAxkDAAI4PWNtZAYp5RXbOKe2_RQkDLNHRnQsAAJxAgACX1eZAAEe_yu4DVELEisE",
+ "g_8": "CAACAgQAAxkDAAI4PmNtZAZuRr1ubCO9SBPYf5uVwxOVAAJzAgACX1eZAAH26plyNxWZuCsE",
+ "g_9": "CAACAgQAAxkDAAI4P2NtZAZ-4ux439AfgakLYhj7NkL7AAJ1AgACX1eZAAGrwYoTMk8UPSsE",
+ "g_draw": "CAACAgQAAxkDAAI4QGNtZAcDJt3SZBIXhpzxAw-0pCjgAAJ3AgACX1eZAAFnlFIJWhbZIysE",
+ "g_skip": "CAACAgQAAxkDAAI4QWNtZAdu6EvL3cTpvKgvVvS5TM8oAAJ7AgACX1eZAAFO5CqgPxquYSsE",
+ "g_reverse": "CAACAgQAAxkDAAI4QmNtZAhYEij-J99P6WZprlvTrO1FAAJ5AgACX1eZAAE9cd3JVwlSEisE",
+ "r_0": "CAACAgQAAxkDAAI4Q2NtZAhJMx2vsEJ0VqZf4K4vnICEAAJ9AgACX1eZAAEZAg2nRervSCsE",
+ "r_1": "CAACAgQAAxkDAAI4RGNtZAggA5W5F360ygp-Kt5511ZGAAJ_AgACX1eZAAFtLPMD6heoDysE",
+ "r_2": "CAACAgQAAxkDAAI4RWNtZAneP8mxTRUYpxCIcSZxrRzaAAKBAgACX1eZAAGuvzFU0Su89SsE",
+ "r_3": "CAACAgQAAxkDAAI4RmNtZAkm-2Z3z4dgngqsNQKlAAEUIgACgwIAAl9XmQABBRY8MBWexokrBA",
+ "r_4": "CAACAgQAAxkDAAI4R2NtZAr32JAr0Q5mSzPrZuPKAAEMAAOFAgACX1eZAAHZFzRnwree-ysE",
+ "r_5": "CAACAgQAAxkDAAI4SGNtZAo06aPW8Bt2bEfhuAwYIAihAAKHAgACX1eZAAHsdpjtu9I2ISsE",
+ "r_6": "CAACAgQAAxkDAAI4SWNtZArDcMo4iVhDv3V2PkjmODGWAAKJAgACX1eZAAG2D__a-tqZBSsE",
+ "r_7": "CAACAgQAAxkDAAI4SmNtZAsNc-unKFxRAUfRgRpIu8zGAAKLAgACX1eZAAGXaAtw5YFztSsE",
+ "r_8": "CAACAgQAAxkDAAI4S2NtZAtXBBjw_QmbUnPCqOjcPciqAAKNAgACX1eZAAGkCOaURWQl8CsE",
+ "r_9": "CAACAgQAAxkDAAI4TGNtZAxdvNd9s7XbaETEDpraDSB8AAKPAgACX1eZAAH-WS6bmv9CgSsE",
+ "r_draw": "CAACAgQAAxkDAAI4TWNtZAz-9sSylYycGwF82_5ceXLOAAKRAgACX1eZAAF2dldgt636fysE",
+ "r_skip": "CAACAgQAAxkDAAI4TmNtZAwwZq3xqWgdKCELX9yXNNDHAAKVAgACX1eZAAGedr9LYgVebCsE",
+ "r_reverse": "CAACAgQAAxkDAAI4T2NtZA1_h1jpVObJt7ZnGWC0EJu_AAKTAgACX1eZAAECR8T0lu-KmysE",
+ "y_0": "CAACAgQAAxkDAAI4UGNtZA3XHBEqHJ4oD2s1vu019fCAAAKXAgACX1eZAALmpUbJzkaKKwQ",
+ "y_1": "CAACAgQAAxkDAAI4UWNtZA70oPDw_EYnua3I_yHnoU0HAAKZAgACX1eZAAGB_02-C22PkysE",
+ "y_2": "CAACAgQAAxkDAAI4UmNtZA73r_BBydbo0QL4Lrp6zzRgAAKbAgACX1eZAAHVmZUJxJwqmCsE",
+ "y_3": "CAACAgQAAxkDAAI4U2NtZA7ITY2cWf3hZhbqbRFA2rznAAKdAgACX1eZAAGnajv8YZQj-ysE",
+ "y_4": "CAACAgQAAxkDAAI4VGNtZA_w89jaIqKJT3mJ3jf4sNfqAAKfAgACX1eZAAEmxeENpAa35SsE",
+ "y_5": "CAACAgQAAxkDAAI4VWNtZA9pJt03yLW1UVqmabBu03CRAAKhAgACX1eZAAH2evQmPPzx8isE",
+ "y_6": "CAACAgQAAxkDAAI4VmNtZBBLaA_cEcY1-cmo4oRl7kFUAAKjAgACX1eZAAGYOfBpuoRg_CsE",
+ "y_7": "CAACAgQAAxkDAAI4V2NtZBC1E-0IzKlEqkiFlLtGQ2djAAKlAgACX1eZAAFYxwrVWROuiysE",
+ "y_8": "CAACAgQAAxkDAAI4WGNtZBDuCE40_AciHh4BlfOxvd4EAAKnAgACX1eZAAF10j1L6rASCSsE",
+ "y_9": "CAACAgQAAxkDAAI4WWNtZBERcGe9cafGmVQMrn--6VyEAAKpAgACX1eZAAGV1nEmuqjoJCsE",
+ "y_draw": "CAACAgQAAxkDAAI4WmNtZBHW7Ik5O4gDp80GEnME_8opAAKrAgACX1eZAAGfJ2XK_ooNFisE",
+ "y_skip": "CAACAgQAAxkDAAI4W2NtZBLpZ4ilI48Wl42H2--LNZleAAKvAgACX1eZAAEVSSkTcHxJXCsE",
+ "y_reverse": "CAACAgQAAxkDAAI4XGNtZBJeXdZLAWEB9hQVadvba2mLAAKtAgACX1eZAAEiP9aakPoiDysE",
+ "draw_four": "CAACAgQAAxkDAAI4XWNtZBOEsZAZxOHFAttWBmLf5WSOAAJhAgACX1eZAAHWx9PCWaCqkysE",
+ "colorchooser": "CAACAgQAAxkDAAI4XmNtZBPR9vYmNzz7P7Hq24wrLE16AAJfAgACX1eZAAH4WHYrSCRGIisE"
+ },
+ "CARDS": {
+ "SPECIALS": ["draw_four","colorchooser"],
+ "SPECIALS_INFO": {
+ "draw_four": [4,"."],
+ "colorchooser": [4,"^(?!.*draw).*$"]
+ },
+ "THEME_CARDS": [],
+ "COLORS": ["b","g","r","y"],
+ "COLOR_ICONS": {
+ "b": "🟦",
+ "g": "🟩",
+ "r": "🟥",
+ "y": "🟨",
+ "x": "❓"
+ },
+ "VALUES": ["0","1","2","3","4","5","6","7","8","9","draw","skip","reverse"],
+ "VALUES_ICONS": {
+ "0": "0️⃣",
+ "1": "1️⃣",
+ "2": "2️⃣",
+ "3": "3️⃣",
+ "4": "4️⃣",
+ "5": "5️⃣",
+ "6": "6️⃣",
+ "7": "7️⃣",
+ "8": "8️⃣",
+ "9": "9️⃣",
+ "draw": "+2",
+ "skip": "🚫",
+ "reverse": "🔁",
+ "colorchooser": "🌈",
+ "draw_four": "+4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/cards/colorblind.json b/cards/colorblind.json
new file mode 100644
index 0000000..7326074
--- /dev/null
+++ b/cards/colorblind.json
@@ -0,0 +1,179 @@
+{
+ "STICKERS": {
+ "b_0": "CAACAgQAAxkBAAIH7WWViIfZuQW92q6yTE6BQfXj-UWDAAL4EAACSPyQUbrUYl3Db1A4HgQ",
+ "b_1": "CAACAgQAAxkBAAIH8GWViIgsVZgNZsZYg_RaskmJCKJZAAJxEwAC74SYUVPWLgtyo7g5HgQ",
+ "b_2": "CAACAgQAAxkBAAIH82WViIklUQmKnrock1_yNBqqMhVJAALTFAACpWGZUfKsVIBvG1-PHgQ",
+ "b_3": "CAACAgQAAxkBAAIH9mWViIrIYOGeBv0MxhC6FWldvYQyAAI2DQACf7GYUd6_wh0X-J7THgQ",
+ "b_4": "CAACAgQAAxkBAAIH-WWViIvJL4MFx0Q10L4Y7J6tTgzOAAKVDgACNlqZUVwgv-lUHlwXHgQ",
+ "b_5": "CAACAgQAAxkBAAIH_GWViIuh16wb9Nb28sRG5uSV6ab5AAKuDAAC4vmZUe8QFnwIbmnlHgQ",
+ "b_6": "CAACAgQAAxkBAAIH_2WViIzaZQQttfD0R03vcmRiKyQWAAIOFQAC4ZGZUZBi-YQ7FzMEHgQ",
+ "b_7": "CAACAgQAAxkBAAIIAmWViI0Ur0TXzVyntbIFiUF70ngzAAIjEQAC_92YUS5W3f2SSBmDHgQ",
+ "b_8": "CAACAgQAAxkBAAIIBWWViI5BriPVz45ADl9kKBP73JTkAALbDQACgTqRUcbtaxxXgXaHHgQ",
+ "b_9": "CAACAgQAAxkBAAIICGWViI8P2ffbeW1mFnEG4sjeJ58MAAK9DQACtEyZUStZpJE_-CIpHgQ",
+ "b_draw": "CAACAgQAAxkBAAIIC2WViJGgz0IpM8FbgtwmtZVLeuj6AAKXDgACMX-QUc9ZKhgoHC3BHgQ",
+ "b_skip": "CAACAgQAAxkBAAIIDmWViJIq_Ib0JQ7UQ5clASSpFNuyAAL5DAACBtmYUaW1SWeyUM1RHgQ",
+ "b_reverse": "CAACAgQAAxkBAAIIEWWViJOq0hbTlIjYSLZw-7ct8OquAAKiDwACoMmZUQlCnlahydSdHgQ",
+ "g_0": "CAACAgQAAxkBAAIIFGWViJbNqv-dn_a5j2LuBSFhGKOPAAJODgAChDGYUXTuNH-98HjGHgQ",
+ "g_1": "CAACAgQAAxkBAAIIF2WViJd4nHU1y1ztouceDT1cfDHuAAJCDgACGSCYUaWyJ_2c21fCHgQ",
+ "g_2": "CAACAgQAAxkBAAIIGmWViJipH6ctdppuXX21KV-hUEl2AALYFQAC57-YUYQ2I9KNnFuJHgQ",
+ "g_3": "CAACAgQAAxkBAAIIHWWViJm1DwWP9SJoSSP-YCAvhaGjAAKnDgACM72YUcA7PwoPafsHHgQ",
+ "g_4": "CAACAgQAAxkBAAIIIGWViJrgai_4_1GWuNTq1SrwHoY6AAJGEAACoXuQURzAtqUt8N9oHgQ",
+ "g_5": "CAACAgQAAxkBAAIII2WViJuQB3lW1m169IogS1bu-dd2AAICDwACpcuYUcnKDYX_Jtu_HgQ",
+ "g_6": "CAACAgQAAxkBAAIIJmWViJ02ZaybQAg82BqZBXYSRQ_xAAJWFgAC0ruZUaKBGr8SQK5WHgQ",
+ "g_7": "CAACAgQAAxkBAAIIKWWViJ55lDw6091FOyalYKntdBRkAALZEgACtxyYUSP7wshU0BuyHgQ",
+ "g_8": "CAACAgQAAxkBAAIILGWViKIstE4URA2mdsL1qn6GVf-cAALoDAAC6-aYUcFFqsyKY5ZrHgQ",
+ "g_9": "CAACAgQAAxkBAAIIL2WViKVGxSTn0Do5onLQ3Ofzka4LAAITEQACw2SZUWvwNocY7CyCHgQ",
+ "g_draw": "CAACAgQAAxkBAAIIMmWViKdf2wdcUNpvrsc_u8cyzSpnAAKEDwACvFiZUf1yHkmnTQABuh4E",
+ "g_skip": "CAACAgQAAxkBAAIINWWViKiQLKGbJaNtyCoxw_Qyc0lDAAJsFwACqKeRUQ7CQm0NF-TaHgQ",
+ "g_reverse": "CAACAgQAAxkBAAIIOGWViKlE1G7Dq9BINwhi5e17RV_dAAIyEAAC9U6YUYfbzgshjtx6HgQ",
+ "r_0": "CAACAgQAAxkBAAIIO2WViKrN7GshtcQ44nYDgpXlvqQ9AALoDwACf9yRUc9YeqIFH_eMHgQ",
+ "r_1": "CAACAgQAAxkBAAIIPmWViKsB7yhwRpa-SkjkqIfqiDFeAALlDQACD4iZUTH5uVAjzxGtHgQ",
+ "r_2": "CAACAgQAAxkBAAIIQWWViKx4deWPc3S0fDskZsyKZ5D1AALWDQAC4yOZUSRCxWE3j0MwHgQ",
+ "r_3": "CAACAgQAAxkBAAIIRGWViK6lgng5gfXIYThsk1g76KS0AAKWEAACrLiZUdai-hpZ9Ab-HgQ",
+ "r_4": "CAACAgQAAxkBAAIIR2WViK_83kuSnMJ8nFSoi6VDxkAFAAIJGgACp_-RUWeXwAIvWuOnHgQ",
+ "r_5": "CAACAgQAAxkBAAIISmWViLIafepcDzkS08BMqkq7rZE3AAJXDwACOZqZUQS_ZNV4v1ylHgQ",
+ "r_6": "CAACAgQAAxkBAAIITWWViLWZySDZcTEnc66aLKx9URsxAAIdDgAC65SRUfYj7KfqdEFUHgQ",
+ "r_7": "CAACAgQAAxkBAAIIUGWViLZK1GPE83RP8VZ1G134HkzHAAI1DwACs_WRUXmuoe2TSlD4HgQ",
+ "r_8": "CAACAgQAAxkBAAIIU2WViLjJbF2IM6EfGh7Fi7Eylv_hAALUEAAC5AORUQ-Iih1SLpa_HgQ",
+ "r_9": "CAACAgQAAxkBAAIIVmWViLlb9lZw4rLF8zE92W492Y2WAALZDgACrWeQUczrPkLQp9_ZHgQ",
+ "r_draw": "CAACAgQAAxkBAAIIWWWViLqtM1UOYophQ9hlsIO-6CR2AAK9DwACpm6QUYSaRFKhJlATHgQ",
+ "r_skip": "CAACAgQAAxkBAAIIXGWViLw3gedDiTOUIuNBrUdTOaWNAAJGDgACnxuZUU4LrF0RllHDHgQ",
+ "r_reverse": "CAACAgQAAxkBAAIIX2WViL3N06HgWmOmAWwRTuf19J92AALlEAACDz2ZUTz4Rj_YOJ9wHgQ",
+ "y_0": "CAACAgQAAxkBAAIIYmWViL9dzEK2pkuaVGZDpGLHIh3aAAKtDgACvlaZUc1_r14GfZg4HgQ",
+ "y_1": "CAACAgQAAxkBAAIIZWWViMDADxyaFhJtHoj2OqLeJuXmAAJyDwACaoqZUd9V5Qje7-LsHgQ",
+ "y_2": "CAACAgQAAxkBAAIIaGWViMGoxYdaIaoz-5lQlAABhnZCEgACkA4AAuDImFEQ8qjFlcKplR4E",
+ "y_3": "CAACAgQAAxkBAAIIa2WViMLZtKOb5eAEYKPfbGQT4AXoAAL5DAACauiYUYCpWf5jw3vbHgQ",
+ "y_4": "CAACAgQAAxkBAAIIbmWViMIZL-SbEFuof3HZiwKxQOM-AAKNDgACY0uYUbzSTuR-DHU0HgQ",
+ "y_5": "CAACAgQAAxkBAAIIcWWViMP8mHHUUxjQTzZME_UqgOucAAKpDwACaBiYUfX97L9sxA1jHgQ",
+ "y_6": "CAACAgQAAxkBAAIIdGWViMQBqzJYS4zfGHLbPwac4xMnAAJ1DQAC5ZyYURsb-CbXAZgJHgQ",
+ "y_7": "CAACAgQAAxkBAAIId2WViMUli35f0n4JpRxn2z7TsxIBAALpDAAClAABmFEjw59G8tfe-R4E",
+ "y_8": "CAACAgQAAxkBAAIIemWViMYHsYK8A8zotkuQL7N9CUGqAAIBEAACL7aRUSJ8n471ZWKWHgQ",
+ "y_9": "CAACAgQAAxkBAAIIfWWViMbbYCRyvT1RHSb5LAxtpQFcAALEDQACFDyYUeOeX21QIdIsHgQ",
+ "y_draw": "CAACAgQAAxkBAAIIgGWViMeJKRPlJmIbw7w6-5EyzSRFAALPDgACdxmZUSEuFfXt6pfwHgQ",
+ "y_skip": "CAACAgQAAxkBAAIIg2WViMmxpEHw_qCaJGGFGv3FCvokAAI-FgACIZuQUb2Gm0U8uCPvHgQ",
+ "y_reverse": "CAACAgQAAxkBAAIIhmWViMomxSr2ZFGL4S45yqm9XlMRAAJPEAACqAWZUQlEHDh5aCcPHgQ",
+ "draw_four": "CAACAgQAAxkBAAIIiWWViM99yb-oIkGdZl69f-zLrDcjAAJhEAACueSZUaapceGNYQHEHgQ",
+ "colorchooser": "CAACAgQAAxkBAAIIjGWViNByhKosbeWF0U5byjtnNxQCAAKuDgAC9faZUSnH8GIMgTmdHgQ",
+ "option_draw": "CAACAgQAAxkBAAIIj2WViNj1FXsNWL95ZRUbPZJXJoX7AAL4AgACX1eZAAH-TdXSlvEa2x4E",
+ "option_pass": "CAACAgQAAxkBAAIIkmWViN0_ybsx490zyv0HFLw2sOzzAAL6AgACX1eZAAFuilR5QnD-Vx4E",
+ "option_bluff": "CAACAgQAAxkBAAIIlWWViOawFS22nDjGn3cB6svJ7ggkAALKAgACX1eZAAHBw478rNqN0B4E",
+ "option_info": "CAACAgQAAxkBAAIImGWViPjlUoOmQw7LWRFlgDq2a5u8AALEAgACX1eZAAGi2Qy93IIQwh4E"
+ },
+ "STICKERS_GREY": {
+ "b_0": "CAACAgQAAxkBAAIIm2WViPpU7jFNtr5MybalgKn-vxneAAJFAgACX1eZAAHwXYFNZhQaIx4E",
+ "b_1": "CAACAgQAAxkBAAIInmWViPtl57X2VaM5XSb6nr1YGSqZAAJHAgACX1eZAAF_ZxC64wgdNB4E",
+ "b_2": "CAACAgQAAxkBAAIIoWWViP0cJwFXikbDcrJ00t1K_xJXAAJJAgACX1eZAAF-GuNgJ25IAAEeBA",
+ "b_3": "CAACAgQAAxkBAAIIpGWViP4BDkd8vKhBxGnHvbjRu_8CAAJLAgACX1eZAAHIJQ71XJ39mB4E",
+ "b_4": "CAACAgQAAxkBAAIIp2WViP7A3X1R9vjYv0bmvOY-rUPIAAJNAgACX1eZAAEjmR2mhJ8SsR4E",
+ "b_5": "CAACAgQAAxkBAAIIqmWViP9fFkit6AU8yMRBi0nbvU6CAAJPAgACX1eZAAEjfAwvM_8nsR4E",
+ "b_6": "CAACAgQAAxkBAAIIrWWViQABRYTEtaNJlrWkobC-DWakRQACUQIAAl9XmQABv35eqFpp188eBA",
+ "b_7": "CAACAgQAAxkBAAIIsGWViQEyYkmw3Hp_6yEuDEZPg_UGAAJTAgACX1eZAAG_xVqK-u2dzB4E",
+ "b_8": "CAACAgQAAxkBAAIIs2WViQHf7qbfR_X1dOxCxDyKQVJ6AAJVAgACX1eZAAF8hUb4bS_NdB4E",
+ "b_9": "CAACAgQAAxkBAAIItmWViQLMnaQOj_1KJ4QB7v7CtgmlAAJXAgACX1eZAAGXAmJ0BKvi1x4E",
+ "b_draw": "CAACAgQAAxkBAAIIuWWViQQt6MhXUnHrxX6ujGLp9OoUAAJZAgACX1eZAAFS-DsDXK7zdh4E",
+ "b_skip": "CAACAgQAAxkBAAIIvGWViQXzcWvBVYYuv8gUbBvJmWNzAAJdAgACX1eZAAFzsBiTRsZIeh4E",
+ "b_reverse": "CAACAgQAAxkBAAIIv2WViQYnXthYtIst_A_lYy9wsFJUAAJbAgACX1eZAAHRLf8w4EEJfx4E",
+ "g_0": "CAACAgQAAxkBAAIIwmWViQfpxDfIp0PyGemF3zYUIOmgAAJjAgACX1eZAAG_c8FzjSBlOB4E",
+ "g_1": "CAACAgQAAxkBAAIIxWWViQgejt9YHBrForCHSWyvLnqtAAJlAgACX1eZAAH2R3CHmHduZB4E",
+ "g_2": "CAACAgQAAxkBAAIIyGWViQnnGcwMI9BRuuD5XkWTP2xQAAJnAgACX1eZAAHB14u8vZ5pjR4E",
+ "g_3": "CAACAgQAAxkBAAIIy2WViQq0QobbZleDp9ZJpFEbdmrMAAJpAgACX1eZAAFaZGnJmMcN9B4E",
+ "g_4": "CAACAgQAAxkBAAIIzmWViQpJScW-f97n0rQpOSOYYAeSAAJrAgACX1eZAAF3KxLEqQq8Kx4E",
+ "g_5": "CAACAgQAAxkBAAII0WWViQteMUvnKyrbndV_TWRSpMuKAAJtAgACX1eZAAGObwogvTEInB4E",
+ "g_6": "CAACAgQAAxkBAAII1GWViQxEufLsZ6qPMt_0muYHN0c4AAJvAgACX1eZAAEpOGFMRnLGmR4E",
+ "g_7": "CAACAgQAAxkBAAII12WViQ38LxBaAsypjKdrLorXOGB1AAJxAgACX1eZAAEe_yu4DVELEh4E",
+ "g_8": "CAACAgQAAxkBAAII2mWViQ5o2Qj2SD-2uqkvRLIkpY74AAJzAgACX1eZAAH26plyNxWZuB4E",
+ "g_9": "CAACAgQAAxkBAAII3WWViQ4X_eX6VuMP3gmi0Z8WUiIPAAJ1AgACX1eZAAGrwYoTMk8UPR4E",
+ "g_draw": "CAACAgQAAxkBAAII4GWViQ-wjvGkjoKeN7G_VN7QeAbFAAJ3AgACX1eZAAFnlFIJWhbZIx4E",
+ "g_skip": "CAACAgQAAxkBAAII42WViRBlqJS24Dpe9jckZjx9NhFrAAJ7AgACX1eZAAFO5CqgPxquYR4E",
+ "g_reverse": "CAACAgQAAxkBAAII5mWViRGErVTAmwzzL8Gaw6m1uEJWAAJ5AgACX1eZAAE9cd3JVwlSEh4E",
+ "r_0": "CAACAgQAAxkBAAII6WWViRPUrdPmRLxJB_wPYSEz9VzJAAJ9AgACX1eZAAEZAg2nRervSB4E",
+ "r_1": "CAACAgQAAxkBAAII7GWViRTYSpKji8JFsJ7grOJpqR63AAJ_AgACX1eZAAFtLPMD6heoDx4E",
+ "r_2": "CAACAgQAAxkBAAII72WViRSJl1uLaIrhIlvDHumJ9WW7AAKBAgACX1eZAAGuvzFU0Su89R4E",
+ "r_3": "CAACAgQAAxkBAAII8mWViRWQGHlRWTZ9izrjtocY12kKAAKDAgACX1eZAAEFFjwwFZ7GiR4E",
+ "r_4": "CAACAgQAAxkBAAII9WWViRYsYsaPYMtVNqEQI14epvFuAAKFAgACX1eZAAHZFzRnwree-x4E",
+ "r_5": "CAACAgQAAxkBAAII-GWViRc1hhpZfuknLhDcTFE4fAwkAAKHAgACX1eZAAHsdpjtu9I2IR4E",
+ "r_6": "CAACAgQAAxkBAAII-2WViRhw6hgkC2hTtz5M01PWq4iaAAKJAgACX1eZAAG2D__a-tqZBR4E",
+ "r_7": "CAACAgQAAxkBAAII_mWViRh0xCPOW62vVVX3eW8qVcYXAAKLAgACX1eZAAGXaAtw5YFztR4E",
+ "r_8": "CAACAgQAAxkBAAIJAWWViRna-E8mEMrtaJgEN6kadagKAAKNAgACX1eZAAGkCOaURWQl8B4E",
+ "r_9": "CAACAgQAAxkBAAIJBGWViRpz1cu-hntkeMicpiUWFV1fAAKPAgACX1eZAAH-WS6bmv9CgR4E",
+ "r_draw": "CAACAgQAAxkBAAIJB2WViRvF9IaFuwLG8Aml_lXaIuVUAAKRAgACX1eZAAF2dldgt636fx4E",
+ "r_skip": "CAACAgQAAxkBAAIJCmWViRz7AyvfbVzBEGknm69vAlgCAAKVAgACX1eZAAGedr9LYgVebB4E",
+ "r_reverse": "CAACAgQAAxkBAAIJDWWViR3n0qcDtix_O4w8xWjd9U_1AAKTAgACX1eZAAECR8T0lu-Kmx4E",
+ "y_0": "CAACAgQAAxkBAAIJEGWViR87oSq_59nWAAFy01lTX4JdxgAClwIAAl9XmQAC5qVGyc5Gih4E",
+ "y_1": "CAACAgQAAxkBAAIJE2WViSD6-Sq3oTIMqVz-J9zOmIddAAKZAgACX1eZAAGB_02-C22Pkx4E",
+ "y_2": "CAACAgQAAxkBAAIJFmWViSEE44UOkY4s8r0LLBtvmjcbAAKbAgACX1eZAAHVmZUJxJwqmB4E",
+ "y_3": "CAACAgQAAxkBAAIJGWWViSGVT72rqzZe98CDc4lP66KnAAKdAgACX1eZAAGnajv8YZQj-x4E",
+ "y_4": "CAACAgQAAxkBAAIJHGWViSLM42LKJuClX_EVkJIUqhb8AAKfAgACX1eZAAEmxeENpAa35R4E",
+ "y_5": "CAACAgQAAxkBAAIJH2WViSMRJDYER1V6jctmpe4_LnjBAAKhAgACX1eZAAH2evQmPPzx8h4E",
+ "y_6": "CAACAgQAAxkBAAIJImWViSTTa8PENvS4oBsjCjyxsOq0AAKjAgACX1eZAAGYOfBpuoRg_B4E",
+ "y_7": "CAACAgQAAxkBAAIJJWWViSX62G9bAiolY1qdo7wt-lu_AAKlAgACX1eZAAFYxwrVWROuix4E",
+ "y_8": "CAACAgQAAxkBAAIJKGWViSYRfMC3KTuSH8FBi3GuERF3AAKnAgACX1eZAAF10j1L6rASCR4E",
+ "y_9": "CAACAgQAAxkBAAIJK2WViSaZDLJsteU3mN3JaZgxbUiQAAKpAgACX1eZAAGV1nEmuqjoJB4E",
+ "y_draw": "CAACAgQAAxkBAAIJLmWViSfj0w5rRfqv3G7Ehafi-cmIAAKrAgACX1eZAAGfJ2XK_ooNFh4E",
+ "y_skip": "CAACAgQAAxkBAAIJMWWViSiyvVRcF4fuiitl0tKoZyZ1AAKvAgACX1eZAAEVSSkTcHxJXB4E",
+ "y_reverse": "CAACAgQAAxkBAAIJNGWViSkfoWKkDlIjj5qEdo5I-W0QAAKtAgACX1eZAAEiP9aakPoiDx4E",
+ "draw_four": "CAACAgQAAxkBAAIJN2WViSw_fFBkTPJ6F984sRBdaXBDAAJhAgACX1eZAAHWx9PCWaCqkx4E",
+ "colorchooser": "CAACAgQAAxkBAAIJOmWViS0VcoFfcxzhI8MIvv7NuJZXAAJfAgACX1eZAAH4WHYrSCRGIh4E"
+ },
+ "CARDS": {
+ "SPECIALS": [
+ "draw_four",
+ "colorchooser"
+ ],
+ "SPECIALS_INFO": {
+ "draw_four": [
+ 4,
+ "."
+ ],
+ "colorchooser": [
+ 4,
+ "^(?!.*draw).*$"
+ ]
+ },
+ "THEME_CARDS": [],
+ "COLORS": [
+ "b",
+ "g",
+ "r",
+ "y"
+ ],
+ "COLOR_ICONS": {
+ "b": "blue",
+ "g": "green",
+ "r": "red",
+ "y": "yellow"
+ },
+ "VALUES": [
+ "0",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "draw",
+ "skip",
+ "reverse"
+ ],
+ "VALUES_ICONS": {
+ "0": "0️⃣",
+ "1": "1️⃣",
+ "2": "2️⃣",
+ "3": "3️⃣",
+ "4": "4️⃣",
+ "5": "5️⃣",
+ "6": "6️⃣",
+ "7": "7️⃣",
+ "8": "8️⃣",
+ "9": "9️⃣",
+ "draw": "+2",
+ "skip": "🚫",
+ "reverse": "🔁",
+ "colorchooser": "🌈",
+ "draw_four": "+4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/cards/minimalist.json b/cards/minimalist.json
new file mode 100644
index 0000000..2aca3c7
--- /dev/null
+++ b/cards/minimalist.json
@@ -0,0 +1,190 @@
+{
+ "STICKERS": {
+ "b_0": "CAACAgEAAxkDAAEDe6tllA16pjgNwKfYPSG2fa3-RWCLZgACAwIAAsJ8kUXyk7MLF9xdBh4E",
+ "b_1": "CAACAgEAAxkDAAEDe6xllA18NuZNUwJvWlQd4oM1ET_DGQACqQMAAnHukEXmnSMIA9EHZh4E",
+ "b_2": "CAACAgEAAxkDAAEDe61llA1_v5dlOv1S_43ufmWuI9jGKwACXQIAAv_qkEVgclfo22dTXx4E",
+ "b_3": "CAACAgEAAxkDAAEDe65llA2By62MlWJL1KoSwAABXX6FJ8MAAhIDAAJ0vpBF7s5S-tulgzceBA",
+ "b_4": "CAACAgEAAxkDAAEDe69llA2EotC8Sxq2BV0_UNLFHiCraAACZwIAAhZvkUVhDXOLwAahsR4E",
+ "b_5": "CAACAgEAAxkDAAEDe7BllA2GMRqzd1XwtbPsB21ysznGggACKgMAAl3TkUU4NWqARyrisR4E",
+ "b_6": "CAACAgEAAxkDAAEDe7FllA2IGUW-kUGfNUNo1yLkEhlTKwACZQIAAkEamUXoN8eyf3WbVR4E",
+ "b_7": "CAACAgEAAxkDAAEDe7JllA2Kl-KJ4CzGnWR6bvmWdsuT3gACPgIAAr9DkUXKlfdmjxSZUx4E",
+ "b_8": "CAACAgEAAxkDAAEDe7NllA2MWfQmdO8qYnV-xd9Tm8pblgAC2AIAAqIskUUWu-bHbGOF8R4E",
+ "b_9": "CAACAgEAAxkDAAEDe7RllA2OcK42iYTR8YHEJLvqzqgZCAACkwIAAgbhkEW3VEnx24JUBh4E",
+ "b_draw": "CAACAgEAAxkDAAEDe7VllA2eJar5j6ts-sL1xCCZBaCKCQACBgMAAkYMkUVyqsDxtyrruR4E",
+ "b_skip": "CAACAgEAAxkDAAEDe7ZllA2gqoH1ykM-E62WG4JKVb0evwAC5AIAAoMOkUXTMcadXYpdLR4E",
+ "b_reverse": "CAACAgEAAxkDAAEDe7dllA2ing8hJqUzPezMWG-u9NJhlAACAwMAAlFCkEUfaBJjzqysoR4E",
+ "g_0": "CAACAgEAAxkDAAEDe7hllA2lB0XmfQX5HerEZEz37IILdwACcQIAAswfkUVCj6ddvIx5iR4E",
+ "g_1": "CAACAgEAAxkDAAEDe7lllA2nl8MoGoxPec0DhQ_rvhBoLAACDwMAApnYkEU_b4TvaFm6-B4E",
+ "g_2": "CAACAgEAAxkDAAEDe7pllA2pP4FIrMKVOnCgvgesr5hhOQACSAIAAruTkEXqutZPzXqcXx4E",
+ "g_3": "CAACAgEAAxkDAAEDe7tllA2r9sc-h0p5781yXUxmIjG30QACCwIAAqnskEWxoXpxAW_zch4E",
+ "g_4": "CAACAgEAAxkDAAEDe7xllA2tI7jBpElvA-v9_y11syRh5AACQQMAAkh4kUWGlbmFagXAQB4E",
+ "g_5": "CAACAgEAAxkDAAEDe71llA2vGXcCAxbTfq3quZMhjtfQHgACbgIAApcckUVhphMsBYqfJx4E",
+ "g_6": "CAACAgEAAxkDAAEDe75llA2xHxOm-TiqMLml4YfVgIGHugACPgIAAi_OkEUiL81ENcLfSx4E",
+ "g_7": "CAACAgEAAxkDAAEDe79llA2yzUGRwz_nGdHVmyYKXdtUIAACDQMAAhSVkEVK2TL696mHEh4E",
+ "g_8": "CAACAgEAAxkDAAEDe8BllA20x6V1fiYfPK0AAZubQti3x90AAgkCAAI68ZFFxalNVgrhumweBA",
+ "g_9": "CAACAgEAAxkDAAEDe8FllA22AtfyKfEy2D1TD9yBg_vjhwAC8gIAAvp_kEUHMIge7TYyKB4E",
+ "g_draw": "CAACAgEAAxkDAAEDe8JllA24zAKP_EPj3WxQxWoCPY4edAACRAIAAujlkEXQU7jwZxRZiB4E",
+ "g_skip": "CAACAgEAAxkBAAIM12ZfWhoZlohTFinVWlZsgdV5CCEkAAITAgACuh2QRcLStZTh5J7VHgQ",
+ "g_reverse": "CAACAgEAAxkDAAEDe8RllA29Dre8XGZlqa4AAfSEXjS9LQYAAqsCAAKkLJFFiweqZ2bqu1QeBA",
+ "r_0": "CAACAgEAAxkDAAEDe8VllA3B-RkLpk1nq0jdcwy9BoSkHgACkAIAAloBkUX_0_LhZSJczR4E",
+ "r_1": "CAACAgEAAxkDAAEDe8ZllA3EVB2FIhJGiPIrtAACEeGVkgACggIAAuUMkUXYO2A7fOgafR4E",
+ "r_2": "CAACAgEAAxkDAAEDe8dllA3GdC6M4zzPTt8hxQNIQ7slzgACGAIAAv7xkEXl-k_R6oY76x4E",
+ "r_3": "CAACAgEAAxkDAAEDe8hllA3JRFrTn-sd5PRN4xWCM90tHwACGgMAAuC2kEXPCeJ41Blilx4E",
+ "r_4": "CAACAgEAAxkDAAEDe8lllA3MMqjbGbOk65ZW-iKojTrsFAAC5QIAAr8akUWeAsacvKVD7B4E",
+ "r_5": "CAACAgEAAxkDAAEDe8pllA3O9EyO50Rao1PvMn0fED0i7gAChwIAAhmTkEXI8f4hQtAPgB4E",
+ "r_6": "CAACAgEAAxkDAAEDe8tllA7PL1JfkjtApb9xfoFTDvDd2QACyAEAAnxpkUUXkvlBf7LZaR4E",
+ "r_7": "CAACAgEAAxkDAAEDe8xllA3TwEfjLkRJOk3bCYuMd_BhgAACtAEAAoGNkUW3IDX6hbLIRR4E",
+ "r_8": "CAACAgEAAxkDAAEDe81llA3Whl0dV_B5gN-LYVHHFFzJBAAC9wEAAl1lkEX1oBklhfbG7h4E",
+ "r_9": "CAACAgEAAxkDAAEDe85llA3ZKoQVIMWcAkgHEUebPu1BkwACpAIAAkG5kEWg5cauEqb-Eh4E",
+ "r_draw": "CAACAgEAAxkDAAEDe89llA3bEXr4bslsjfbgKJxX6_soWAACbAMAAiXikEUOrMwjMnq71h4E",
+ "r_skip": "CAACAgEAAxkDAAEDe9BllA3e3VxyE6B9kKt_o0uo3gO0QgAC_gIAAsVPkUXDxwuskYP9wB4E",
+ "r_reverse": "CAACAgEAAxkDAAEDe9FllA3h6JFRqSvdtHK7AzPGKhR93QAC3gIAAqXNkEVHBrFdkXLu8B4E",
+ "y_0": "CAACAgEAAxkDAAEDe9JllA3jVjW8wkE9VFRUdbvukteHigACTgIAAsdskUUJCGkroV6nOx4E",
+ "y_1": "CAACAgEAAxkDAAEDe9NllA3mM3HzFiwpLbIZmn1y7og-ogAC6QEAApAEkEVBGUptZYFNtx4E",
+ "y_2": "CAACAgEAAxkDAAEDe9RllA3pSe6Pq6XfdXrGSUwO_XnNRwAC9QIAAumPkUWO0RtAwk-gqh4E",
+ "y_3": "CAACAgEAAxkDAAEDe9ZllA3yb8nE9KLHDwf4TpLVSQi8PwACKAMAAlZkkEX3DMzHkfKt4x4E",
+ "y_4": "CAACAgEAAxkDAAEDe9dllA31GtSoqcIH83bG6E_-e_LMGwACcwIAAvhqkUVHXHrGg4bzkB4E",
+ "y_5": "CAACAgEAAxkDAAEDe9hllA34eX3HlQVCB3yz4jE_hxkcPgACvAIAAlrSkUXmuFcRjj_Ykh4E",
+ "y_6": "CAACAgEAAxkDAAEDe9lllA38BYT4tiz3UynZ1KlgBeE9zwACjAIAAucGkEUklwtxCimr8h4E",
+ "y_7": "CAACAgEAAxkDAAEDe9pllA3_D9P4Z8gbdSrJolueNEReHgACpAIAAmLqkEW8I1ITQ-K4UR4E",
+ "y_8": "CAACAgEAAxkDAAEDe9tllA4CfpWusm9qPlMzZB4d1OZuOwACDQIAAl0VkEUjRoTZaTeIiR4E",
+ "y_9": "CAACAgEAAxkDAAEDe9xllA4F1NkFzxkbrUN-tdzZP5X7HwACtwIAAleokUWrFCx6aDoY1B4E",
+ "y_draw": "CAACAgEAAxkDAAEDe91llA4IQ-kifRI4cFbGbMZBA3rWvwACVAIAAmnbkUWKjhkSgboWox4E",
+ "y_skip": "CAACAgEAAxkDAAEDe95llA4MjLGS0nj2fCILrVq0VDOu9AAC4gEAAsUYkUVNLlmImNML9h4E",
+ "y_reverse": "CAACAgEAAxkDAAEDe99llA4QK1OuIZxHXrjpHa72GHwnQwACxAIAAn1JkEUTDAHVfy579h4E",
+ "draw_four": "CAACAgEAAxkDAAEDd9tldPH_lqQbjb_fguXSJiUEZmXMDgACCAIAAmfQkEWdQcu_mD_LxR4E",
+ "colorchooser": "CAACAgEAAxkDAAEDd9xldPICMlwKe0ZQLmNenTD1fQ3cNQACaAIAAgeNkUXMnabDbsOjOh4E",
+ "mirror": "CAACAgEAAxkBAAIJjGWXDOVcBxySviDWuqpeCBt2wzsRAALfAgACM5iRRbhn4u7C82kqHgQ",
+ "option_draw": "CAACAgEAAxkDAAEDeJpldQABh5BKyV9yoabNg_K5EBnWbAkAAtYEAAL1-6hHbSd5q2HZFrAeBA",
+ "option_pass": "CAACAgQAAxkDAAI4JmNtY_yMlr6rB3UdTikR3zFCk8kVAAL6AgACX1eZAAFuilR5QnD-VysE",
+ "option_bluff": "CAACAgQAAxkDAAI4J2NtY_2Dmt5Mi4iZhsUh32OeNVe7AALKAgACX1eZAAHBw478rNqN0CsE",
+ "option_info": "CAACAgQAAxkDAAI4KGNtY_3tO0Sxhu5NzF1UA3tdUnklAALEAgACX1eZAAGi2Qy93IIQwisE"
+ },
+ "STICKERS_GREY": {
+ "b_0": "CAACAgEAAxkDAAEDeBhldPQpr_HP5jMfg5eqmF9WwcbANwACzAEAAvMQkUW3xk9Rm7wDdB4E",
+ "b_1": "CAACAgEAAxkDAAEDeBlldPQtK30FApRgW8VPDinDZiBcMwACHAMAAhT5kEU5hLA1PG5mNx4E",
+ "b_2": "CAACAgEAAxkDAAEDeBpldPQy489pxZnGU_OJivHLZUz_rgAC8gIAAhWNkUUj2qPu53BQAR4E",
+ "b_3": "CAACAgEAAxkDAAEDeBtldPQ1QGHrATJf8y6xY1WOESH19wACywIAAoCvkEUusWPJE3SO8R4E",
+ "b_4": "CAACAgEAAxkDAAEDeBxldPQ4gNTK8wbbKPySKhH9HRQjvwACKAIAApa_kEU9N3ivQr1-1x4E",
+ "b_5": "CAACAgEAAxkDAAEDeB1ldPQ7La8udS3TXWrlx0X1-hfj-gACRwIAArP5kEVCQ9gkbL8Byh4E",
+ "b_6": "CAACAgEAAxkDAAEDeB5ldPQ-GnWfnbES5XLjjeEnCCx9QgACKQMAAqPkkUUugGSOjf9txR4E",
+ "b_7": "CAACAgEAAxkDAAEDeB9ldPRC1btFrtNIrvF2r5Cm46WbnQACWQIAAomUmUUvhGYvR8jwPh4E",
+ "b_8": "CAACAgEAAxkDAAEDeCBldPRFe3GVn8NG-kFW-6kCBfbZTwACaAIAAgg5kUUsEUnkv0FafR4E",
+ "b_9": "CAACAgEAAxkDAAEDeCFldPRIRXMxVF6-2SIuN9N2TcYSbwACSgwAAr8jkEXx2Ncg-cKr-B4E",
+ "b_draw": "CAACAgEAAxkDAAEDeCJldPRM9VR0-q4Nr1CH71nRpi3_-AACZQMAAtvQkUWdG5DeUdyYbR4E",
+ "b_skip": "CAACAgEAAxkDAAEDeCNldPRQkadczfkJ1AGbpGhP4hJoYQAClwMAAgQSkEVJkgABSqHf3RMeBA",
+ "b_reverse": "CAACAgEAAxkDAAEDeCRldPRSBY2Xe8gqahJ5uX7jhk72lgACXgIAArfzkUVdqU8Skpie5B4E",
+ "g_0": "CAACAgEAAxkDAAEDeCVldPRWthIVrMEDrpNDQkWocduO_AACcgIAAkpskEV35BNxHr8V5x4E",
+ "g_1": "CAACAgEAAxkDAAEDeCZldPRantCOaUJ5thTqgnk1aiL5QgACCwMAAs3ukUVOx82xQv9Ffx4E",
+ "g_2": "CAACAgEAAxkDAAEDeCdldPRdO7pkiMb9Jiiy5470Z8rUzAACewIAApPKkEUHSvig3w4D6R4E",
+ "g_3": "CAACAgEAAxkDAAEDeChldPRh9Wyk3_ttBrB4dKFW534KJQACNRYAAl_mkUUjJPhaXCrytR4E",
+ "g_4": "CAACAgEAAxkDAAEDeClldPRk_MvviiZl7flLtfzE_oJTLQAC2AIAAkIjkEU6INaWefsnbR4E",
+ "g_5": "CAACAgEAAxkDAAEDeCpldPRmgqPSrQI-xX4Y4tL9-ejIKQACSAMAAsXrkUU8dg1tlvXuIx4E",
+ "g_6": "CAACAgEAAxkDAAEDeCtldPRqS9IxSLuq2bC61gABw2TBEQADfgIAAtKEkEV2q3ae2QfTlR4E",
+ "g_7": "CAACAgEAAxkDAAEDeCxldPRuL0gfAcA0n2sx_f0iv5sctwACaQIAAvBfkEWujcXUoCoNkh4E",
+ "g_8": "CAACAgEAAxkDAAEDeDBldPTZGi65G5L8tGWUO3wOSixVhwACYAMAAr7jkUW_EGWLU2VY3x4E",
+ "g_9": "CAACAgEAAxkDAAEDeDFldPTatuaW2a1roWs1yQ0CknzrFAACZAIAAofZkUV4qNUOIfMGfB4E",
+ "g_draw": "CAACAgEAAxkDAAEDeDJldPTbUcBqKAzvWXOb7hYO3ebsVAAC2QIAAojIkEWLotFWFUrd9B4E",
+ "g_skip": "CAACAgEAAxkDAAEDeDNldPTdh9PvZ_VZ8XIm9Km6bX2J_QACTQIAArBokEVwbQsgazaHHB4E",
+ "g_reverse": "CAACAgEAAxkDAAEDeDRldPTd9yIAAZ4Y6szNLr_KmTCgvCgAAu8CAAKLQJFFVS-Ee507SeQeBA",
+ "r_0": "CAACAgEAAxkDAAEDeDVldPTeA_iz6315Ync-7RV5bjg8lgACMgYAAqQtkEUfzWAEmEoSsx4E",
+ "r_1": "CAACAgEAAxkDAAEDeDZldPTfPZCyQY-NBUz4YHZKVqkFdQACvgIAAhRRkUXPAAFc1XcuDSseBA",
+ "r_2": "CAACAgEAAxkDAAEDeDdldPTf3s_igw3SQLjEpVeg7cxa2AAC8wIAAn5ukEXFPt-unsEYFB4E",
+ "r_3": "CAACAgEAAxkDAAEDeDhldPTgnLoEQVmm8PXRZNjfcKT08wACHQMAAg0HkEUTAAEuHgIFMMgeBA",
+ "r_4": "CAACAgEAAxkDAAEDeDlldPTh80oMZu94S4aEgOHe_ETbzwACMgIAAloEkEU4V1zqccv4px4E",
+ "r_5": "CAACAgEAAxkBAAINHGZfWnsZDbEzrk-2D3HjMV1F65AAA6ACAAI7OJFFF9QnY0HDd4YeBA",
+ "r_6": "CAACAgEAAxkDAAEDeDtldPTjto0-gDKrLfvtOnLR3yedfwAC8QEAAh0PkUWDDCnQ0yKJ4B4E",
+ "r_7": "CAACAgEAAxkDAAEDeDxldPTj_eXDrdZOTuAgDapmmCGopgACjAIAAnyFkUWR0nnKzFE9Ox4E",
+ "r_8": "CAACAgEAAxkDAAEDeD1ldPTksKpKl4svmqhZp754lHk_4AAC1QIAAnjkkEWe22LmFa-TXx4E",
+ "r_9": "CAACAgEAAxkDAAEDeD5ldPTkK9WuHM_JGtcqX82HXids2AACBQQAAlV2kEXV6etMTirPNh4E",
+ "r_draw": "CAACAgEAAxkDAAEDeD9ldPTl95GlyWxd13otkkbwgYxnuQACIQIAArN7kEXAyhLPjeuM0B4E",
+ "r_skip": "CAACAgEAAxkDAAEDeEBldPTmDSmE_hf8iqfvTNw_MhN-DgACjQIAAgFikUUbhvkdjEvL_B4E",
+ "r_reverse": "CAACAgEAAxkDAAEDeEFldPTm_FseoIs4TkycuysDW93ClgACiwIAAvKekEWH4bB4fK_JAh4E",
+ "y_0": "CAACAgEAAxkDAAEDeEJldPUSOSBzeDwybGll4Z8UKWuTzgACzQIAAlZRkEUaN3RtvlPkhx4E",
+ "y_1": "CAACAgEAAxkDAAEDeENldPUTdMKXIDNbaFklvDnV8eyHfQAC_QIAAmS8kUWe4Qhj7djTNx4E",
+ "y_2": "CAACAgEAAxkDAAEDeERldPUTCwm7oP44jKDQXw_0v0EHFwACDAMAAkYEkEXwjgrHAngn8h4E",
+ "y_3": "CAACAgEAAxkDAAEDeEVldPUUK3_GcOUCyoAz9QxD6m21tAAC9AMAAuu9kUVv0c8dISk1Ix4E",
+ "y_4": "CAACAgEAAxkDAAEDeEZldPUUxdlEgfu7gmJvazzHtiVjLQACbwQAAp9KkUWXChzRPGdXKh4E",
+ "y_5": "CAACAgEAAxkDAAEDeEdldPUVJRjFaF2gCyeb9FI5eLHJYwACFQMAAqTfkUWYvUcRvcNxhh4E",
+ "y_6": "CAACAgEAAxkDAAEDeEhldPUWzqtfWclTzKpnM9poibfNIQACpgIAAhZ3kEW9Xv1BRth-4B4E",
+ "y_7": "CAACAgEAAxkDAAEDeElldPUWLMeeIuoZ_vL46CgPfzDTxwAC7QEAApxekEUmO5PhRBZWdx4E",
+ "y_8": "CAACAgEAAxkDAAEDeEpldPUXs-BExZa_58xEDvMRbS4_nAACfgIAAmg-kUUoRbaAW_B4HR4E",
+ "y_9": "CAACAgEAAxkDAAEDeEtldPUYcpBSMEHnXZkFkK6mAyF49AACqwIAAtsVkEVRqf1U9QqfdB4E",
+ "y_draw": "CAACAgEAAxkDAAEDeExldPUYd5oe-E69-C2kjrolu2s64wACqwIAAk1xkUXw_oyRmWZrwx4E",
+ "y_skip": "CAACAgEAAxkDAAEDeE1ldPUZ57bOPol5KNgUnLGEsWOCRQACUAIAAq70mEW_IHyXZAkgxB4E",
+ "y_reverse": "CAACAgEAAxkDAAEDeE5ldPUZlq9iapYBJlTdZv0OJ1HjiQACzwIAAiozkEUGSQFnhRdepR4E",
+ "draw_four": "CAACAgEAAxkDAAEDeE9ldPUmQloxc6_ReBmEx6mUL1GYTwACBAIAAisrmUUd5e1D0lszKB4E",
+ "colorchooser": "CAACAgEAAxkDAAEDeFBldPUxrSNyn9WqB7y9yp7QTeyTaAACDQIAAlZtkEUPHKKL4f24nR4E",
+ "mirror": "CAACAgEAAxkDAAEDeLpldRDJK0FwP0BId9Tnvf_2yBrz7AACBgIAAtSKkUWN6yeT8oqBeB4E"
+ },
+ "CARDS": {
+ "SPECIALS": [
+ "draw_four",
+ "colorchooser",
+ "mirror"
+ ],
+ "SPECIALS_INFO": {
+ "draw_four": [
+ 4,
+ "."
+ ],
+ "colorchooser": [
+ 4,
+ "^(?!.*draw).*$"
+ ],
+ "mirror": [
+ 2,
+ "draw|skip"
+ ]
+ },
+ "THEME_CARDS": [
+ "mirror"
+ ],
+ "COLORS": [
+ "b",
+ "g",
+ "r",
+ "y"
+ ],
+ "COLOR_ICONS": {
+ "b": "🟦",
+ "g": "🟩",
+ "r": "🟥",
+ "y": "🟨",
+ "x": "❓"
+ },
+ "VALUES": [
+ "0",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "draw",
+ "skip",
+ "reverse"
+ ],
+ "VALUES_ICONS": {
+ "0": "0️⃣",
+ "1": "1️⃣",
+ "2": "2️⃣",
+ "3": "3️⃣",
+ "4": "4️⃣",
+ "5": "5️⃣",
+ "6": "6️⃣",
+ "7": "7️⃣",
+ "8": "8️⃣",
+ "9": "9️⃣",
+ "draw": "+2",
+ "skip": "🚫",
+ "reverse": "🔁",
+ "colorchooser": "🌈",
+ "draw_four": "+4",
+ "mirror": "🪞"
+ }
+ }
+}
\ No newline at end of file
diff --git a/config-example.py b/config-example.py
new file mode 100644
index 0000000..c8ecdb0
--- /dev/null
+++ b/config-example.py
@@ -0,0 +1,12 @@
+from hydrogram import Client
+
+
+games = {}
+player_game = {}
+
+API_ID = ''
+API_HASH = ''
+
+#--- Telegram config ---
+
+bot = Client("bot", api_id=API_ID, api_hash=API_HASH, plugins=dict(root="plugins"))
diff --git a/db.py b/db.py
new file mode 100644
index 0000000..220c4bf
--- /dev/null
+++ b/db.py
@@ -0,0 +1,48 @@
+import os
+
+from tortoise import Tortoise, connections, fields
+from tortoise.backends.base.client import Capabilities
+from tortoise.models import Model
+
+
+class Chat(Model):
+ id = fields.IntField(pk=True)
+ theme = fields.CharField(max_length=255, default="classic")
+ bluff = fields.BooleanField(default=True)
+ seven = fields.BooleanField(default=False)
+ one_win = fields.BooleanField(default=False)
+ one_card = fields.BooleanField(default=False)
+ lang = fields.CharField(max_length=255, default="en-US")
+
+class User(Model):
+ id = fields.BigIntField(pk=True)
+ placar = fields.BooleanField(default=False)
+ wins = fields.IntField(default=0)
+ matches = fields.IntField(default=0)
+ cards = fields.IntField(default=0)
+ sudo = fields.BooleanField(default=False)
+ lang = fields.CharField(max_length=255, default="en-US")
+
+
+async def connect_database():
+ await Tortoise.init(
+ {
+ "connections": {
+ "bot_db": os.getenv("DATABASE_URL", "sqlite://database.sqlite")
+ },
+ "apps": {"bot": {"models": [__name__], "default_connection": "bot_db"}},
+ }
+ )
+
+ conn = connections.get("bot_db")
+ conn.capabilities = Capabilities(
+ "sqlite",
+ daemon=False,
+ requires_limit=True,
+ inline_comment=True,
+ support_for_update=False,
+ support_update_limit_order_by=False,
+ )
+
+ # Generate the schema
+ await Tortoise.generate_schemas()
diff --git a/deck.py b/deck.py
new file mode 100644
index 0000000..fa5fb6b
--- /dev/null
+++ b/deck.py
@@ -0,0 +1,23 @@
+import random
+from card import cards
+
+
+class Deck:
+ def __init__(self, theme) -> None:
+ self.cards = [
+ (color, value)
+ for _ in range(2)
+ for color in cards[theme]["CARDS"]["COLORS"]
+ for value in cards[theme]["CARDS"]["VALUES"]
+ ]
+ self.cards += [
+ ("x", car)
+ for car in cards[theme]["CARDS"]["SPECIALS"]
+ for _ in range(cards[theme]["CARDS"]["SPECIALS_INFO"][car][0])
+ ]
+
+ def shuffle(self):
+ random.shuffle(self.cards)
+
+ def draw(self, amount):
+ return [self.cards.pop(0) for _ in range(amount) if self.cards]
diff --git a/game.py b/game.py
new file mode 100644
index 0000000..0f5e717
--- /dev/null
+++ b/game.py
@@ -0,0 +1,24 @@
+from hydrogram.types import Chat, User
+from deck import Deck
+
+
+class Game:
+ def __init__(self, chat: Chat, theme) -> None:
+ self.chat = chat
+ self.last_card = None
+ self.last_card_2 = None
+ self.next_player:User = None
+ self.deck = Deck(theme)
+ self.players = {}
+ self.is_started = False
+ self.draw = 0
+ self.chosen = None
+ self.closed = False
+ self.winner = True
+
+ def next(self):
+ if self.draw >= 0:
+ indice = list(self.players.keys()).index(self.next_player.id)
+ next_ind = (indice + 1) % len(self.players)
+ next_key = list(self.players.keys())[next_ind]
+ self.next_player = self.players[next_key]
diff --git a/locales.py b/locales.py
new file mode 100644
index 0000000..c5afb0a
--- /dev/null
+++ b/locales.py
@@ -0,0 +1,93 @@
+import os
+from functools import partial, wraps
+from glob import glob
+from typing import Dict, List
+from hydrogram.types import Message
+from hydrogram.enums import ChatType
+from config import player_game
+from game import Game
+
+import yaml
+
+from db import User, Chat
+
+langs = ["en-US", "pt-BR"]
+
+default_language = "en-US"
+
+
+def cache_localizations(files: List[str]) -> Dict[str, Dict[str, Dict[str, str]]]:
+ ldict = {lang: {} for lang in langs}
+ for file in files:
+ _, pname = file.split(os.path.sep)
+ lang = pname.split(".")[0]
+ with open(file, "r") as f:
+ ldict[lang] = yaml.safe_load(f)
+ return ldict
+
+
+jsons = []
+
+for locale in langs:
+ jsons += glob(os.path.join("locales", f"{locale}.yml"))
+
+langdict = cache_localizations(jsons)
+
+
+def use_lang():
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(client, message):
+ ulang = (await User.get_or_create(id=message.from_user.id))[0].lang
+ if not isinstance(message, Message):
+ if message.from_user.id not in player_game:
+ clang = ulang
+ else:
+ game: Game = player_game[message.from_user.id]
+ chatid = game.chat.id
+ clang = (await Chat.get_or_create(id=chatid))[0].lang
+ else:
+ clang = (await Chat.get_or_create(id=message.chat.id))[0].lang
+ ulfunc = partial(get_locale_string, ulang)
+ clfunc = partial(get_locale_string, clang)
+ return await func(client, message, ulfunc, clfunc)
+
+ return wrapper
+
+ return decorator
+
+
+def use_chat_lang():
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(client, message):
+ mmessage = message.message if not isinstance(message, Message) else message
+ if mmessage.chat.type == ChatType.PRIVATE:
+ clang = (await User.get_or_create(id=mmessage.chat.id))[0].lang
+ else:
+ clang = (await Chat.get_or_create(id=mmessage.chat.id))[0].lang
+ clfunc = partial(get_locale_string, clang)
+ return await func(client, message, clfunc)
+
+ return wrapper
+
+ return decorator
+
+
+def use_user_lang():
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(client, message):
+ ulang = (await User.get_or_create(id=message.from_user.id))[0].lang
+ ulfunc = partial(get_locale_string, ulang)
+ return await func(client, message, ulfunc)
+
+ return wrapper
+
+ return decorator
+
+
+def get_locale_string(language: str, key: str) -> str:
+ print(f"Getting {key} for {language}")
+ res: str = langdict[language].get(key) or key
+ return res
diff --git a/locales/en-US.yml b/locales/en-US.yml
new file mode 100644
index 0000000..6373c01
--- /dev/null
+++ b/locales/en-US.yml
@@ -0,0 +1,69 @@
+lang_name: English
+lang_flag: 🇺🇸
+lang_changed: The language has been changed to English!
+back: Back
+theme: Theme
+info_theme: You choose a theme for the cards
+seven_zero: 7-0
+info_seven: When a 7 is played, the player can swap cards with another player. When a 0 is played, everyone passes their cards to the next player.
+sbluff: Bluff
+info_bluff: When a player plays a +4 card, the next one can challenge. If the player who played the +4 card has another card to play, they draw 4 cards. If not, the one who challenged draws 6.
+one_win: One winner
+info_one_win: When the first player runs out of cards, the game ends and they are the winner.
+one_card: Say UNO
+info_one_card: When a player has only one card, they must say UNO. If they don't, they must draw 2 cards.
+language: Language
+info_lang: You can change the bot's language.
+choose_lang: Choose a language
+start_text: "Hello! I am the UNO Bot, a bot to play UNO on Telegram!\n\nTo play, put me in a group and send /new to start a new game!\n\nor to configure use the buttons below!"
+game_mode: Game Mode
+status: Status
+game_mode_text: "Showing information of available game modes!\n\n"
+settings: "Welcome to the settings menu!\n\nChoose an option below to configure the bot."
+theme_config: "Choose a theme for the cards:"
+minimalist: Minimalist
+classic: Classic
+colorblind: Colorblind
+game_existis: There is already a game in progress!
+only_group: This command only works in groups!
+already_in_game: You are already in another game
+already_joined: You are already in the game!
+join: Join
+leave: Leave
+start_game: Start Game
+game_started: A new game has started!
+no_game: There is no game in progress!
+no_game_text: No game in progress, send /new to start a new game!
+lobby_closed: The lobby is closed!
+lobby_opened: The lobby is open!
+joined_game: You joined the game!
+player_joined: "{} joined the game!"
+no_joinned: You are not in the game!
+next: Next {}
+game_over: The game is over!
+player_left: "{} left the game!"
+left_game: You left the game!
+not_allowed: You are not allowed to do that!
+game_started: The game has already started!
+info_text: 🃏 Game Information:\nCurrent player - {current_player}\nLast card - {last_card}\n
+info_text2: "{player}: {cards} cards, "
+pass: Pass
+buy: Buy {} card(s)
+bluff: I think you're bluffing!
+cards_swapped: "{name1} swapped cards with {name2}"
+not_swapped: Due to the number of players, it was not possible to swap cards!
+swapped: The cards have been swapped!
+bought: You bought {buy} cards!
+bluffed: "{name} You can't bluff!\n{name} drew {draw} cards!"
+not_bluffed: "{name1} You didn't bluff!\n{name2} drew {draw} cards!"
+colorchoose: "{name} choose a color!"
+colorchoosed: I chose the color {color}!
+playerchoose: "{name} choose a player!"
+invalid_card: Invalid move!
+said_uno: "{name} shouted UNO!"
+say_uno: "{name} say UNO or draw 2 cards!"
+not_said_uno: "{name} You didn't shout UNO!\n{name} drew 2 cards!"
+first: " in first"
+won: "{name} won the game{comp}!"
+continuing: continuing the game with {count} players
+play: Play
diff --git a/locales/pt-BR.yml b/locales/pt-BR.yml
new file mode 100644
index 0000000..380a353
--- /dev/null
+++ b/locales/pt-BR.yml
@@ -0,0 +1,69 @@
+lang_name: Português
+lang_flag: 🇧🇷
+lang_changed: O idioma foi alterado para Português!
+back: Voltar
+theme: Tema
+info_theme: Você escolhe um tema para as cartas
+seven_zero: 7-0
+info_seven: Quando um 7 é jogado, o jogador pode trocar as cartas com outro jogador. Quando um 0 é jogado, todos passam as cartas para o próximo jogador.
+sbluff: Blefe
+info_bluff: Quando um jogador joga uma carta +4, o próximo pode desafiar. Se o jogador que jogou a carta +4 tiver outra carta para jogar, ele compra 4 cartas. Se não tiver, quem desafiou compra 6.
+one_win: Um vencedor
+info_one_win: Quando o primeiro jogador fica sem cartas, o jogo acaba e ele é o vencedor.
+one_card: Dizer UNO
+info_one_card: Quando um jogador tem apenas uma carta, ele deve dizer UNO. Se ele não disser, ele deve comprar 2 cartas.
+language: Idioma
+info_lang: Você pode alterar o idioma do bot.
+choose_lang: Escolha um idioma
+start_text: "Olá! Eu sou o UNO Bot, um bot para jogar UNO no Telegram!\n\nPara jogar me coloque em um grupo e mande /new para começar um novo jogo!\n\nou para configurar use os botões abaixo!"
+game_mode: Modo de Jogo
+status: Status
+game_mode_text: "Mostrando informações dos modos de jogo disponíveis!\n\n"
+settings: "Bem vindo ao menu de configurações!\n\nEscolha uma opçãoe abaixo para configurar o bot."
+theme_config: "Escolha um tema para as cartas:"
+minimalist: Minimalista
+classic: Clássico
+colorblind: Daltônico
+game_existis: Já existe um jogo em andamento!
+only_group: Esse comando só funciona em grupos!
+already_in_game: Você já está em outro jogo
+already_joined: Você já está no jogo!
+join: Entrar
+leave: Sair
+start_game: Começar Jogo
+game_started: Um novo jogo foi iniciado!
+no_game: Não há jogo em andamento!
+no_game_text: Nenhum jogo em andamento, mande /new para iniciar um novo jogo!
+lobby_closed: O lobby está fechado!
+lobby_opened: O lobby está aberto!
+joined_game: Você entrou no jogo!
+player_joined: "{} entrou no jogo!"
+no_joinned: Você não está no jogo!
+next: Próximo {}
+game_over: O jogo acabou!
+player_left: "{} saiu do jogo!"
+left_game: Você saiu do jogo!
+not_allowed: Você não tem permissão para fazer isso!
+game_started: O jogo já começou!
+info_text: 🃏 Informações do Jogo:\nJogador atual - {current_player}\nÚltima carta - {last_card}\n
+info_text2: "{player}: {cards} cartas, "
+pass: Passar
+buy: Comprar {} carta(s)
+bluff: Eu acho que você está blefando!
+cards_swapped: "{name1} trocou as cartas com {name2}"
+not_swapped: Devido ao numero de jogadores, não foi possível trocar as cartas!
+swapped: As cartas foram trocadas!
+bought: Você comprou {buy} cartas!
+bluffed: "{name} Você não pode blefar!\n{name} comprou {draw} cartas!"
+not_bluffed: "{name1} Você não blefou!\n{name2} comprou {draw} cartas!"
+colorchoose: "{name} escolha uma cor!"
+colorchoosed: Eu escolhi a cor {color}!
+playerchoose: "{name} escolha um jogador!"
+invalid_card: Jogada inválida!
+said_uno: "{name} gritou UNO!"
+say_uno: "{name} fale UNO ou compre 2 cartas!"
+not_said_uno: "{name} Você não gritou UNO!\n{name} comprou 2 cartas!"
+first: " em primeiro"
+won: "{name} ganhou o jogo{comp}!"
+continuing: continuando o jogo com {count} jogadores
+play: Jogar
\ No newline at end of file
diff --git a/plugins/game.py b/plugins/game.py
new file mode 100644
index 0000000..dae8333
--- /dev/null
+++ b/plugins/game.py
@@ -0,0 +1,607 @@
+import importlib
+import re
+
+from hydrogram import Client, filters
+from hydrogram.enums import ChatType
+from hydrogram.types import (
+ ChosenInlineResult,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ InlineQueryResultArticle,
+ InlineQueryResultCachedSticker,
+ InputTextMessageContent,
+ Message,
+ InlineQuery,
+ CallbackQuery,
+)
+from hydrogram.errors import ListenerTimeout
+
+from card import COLORS, cards
+from config import games, player_game
+from db import Chat, User
+from game import Game
+from typing import Union
+from locales import use_lang
+
+
+@Client.on_message(filters.command("new"))
+@use_lang()
+async def new_game(c: Client, m: Message, ut, ct):
+ await Chat.get_or_create(id=m.chat.id)
+ if (
+ m.chat.id in games
+ or m.chat.type == ChatType.PRIVATE
+ or player_game.get(m.from_user.id)
+ ):
+ return await m.reply_text(
+ ut("game_existis")
+ if m.chat.id in games
+ else ut("only_group")
+ if m.chat.type == ChatType.PRIVATE
+ else ut("already_in_game")
+ )
+
+ game = Game(m.chat, (await Chat.get(id=m.chat.id)).theme)
+ game.players[m.from_user.id] = m.from_user
+ games[m.chat.id] = game
+ player_game[m.from_user.id] = game
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(ct("join"), callback_data="join_game"),
+ InlineKeyboardButton(ct("leave"), callback_data="leave_game"),
+ ],
+ [InlineKeyboardButton(ct("start_game"), callback_data="start_game")],
+ ]
+ )
+ return await m.reply_text(ct("game_started"), reply_markup=keyb)
+
+
+@Client.on_message(filters.command("join"))
+@Client.on_callback_query(filters.regex("^join_game$"))
+@use_lang()
+async def join_game(c: Client, m: Union[Message, CallbackQuery], ut, ct):
+ if isinstance(m, CallbackQuery):
+ func = m.answer
+ else:
+ func = m.reply_text
+ game: Game = games.get(m.chat.id if isinstance(m, Message) else m.message.chat.id)
+ if not game or m.from_user.id in game.players or player_game.get(m.from_user.id):
+ return await func(
+ ut("no_game")
+ if not game
+ else ut("already_joined")
+ if m.from_user.id in game.players
+ else ut('already_in_game')
+ )
+ elif game.closed:
+ return await func(ct("lobby_closed"))
+
+ game.players[m.from_user.id] = m.from_user
+ player_game[m.from_user.id] = game
+ await func(ut("joined_game"))
+ if isinstance(m, CallbackQuery):
+ await c.send_message(
+ m.message.chat.id, ct("player_joined").format(m.from_user.mention)
+ )
+
+ if game.is_started:
+ game.players[m.from_user.id].cards = game.deck.draw(7)
+ game.players[m.from_user.id].total_cards = 0
+
+
+@Client.on_callback_query(filters.regex("^leave_game$"))
+@Client.on_message(filters.command("leave"))
+@use_lang()
+async def leave_game(c: Client, m: Union[Message, CallbackQuery], ut, ct):
+ if isinstance(m, CallbackQuery):
+ func = m.answer
+ else:
+ func = m.reply_text
+ game: Game = games.get(m.chat.id if isinstance(m, Message) else m.message.chat.id)
+ if not game or m.from_user.id not in game.players:
+ return await func(
+ ut("no_game") if not game else ut("no_joinned")
+ )
+
+ if game.is_started and game.next_player.id == m.from_user.id:
+ inline_keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(ct("play"), switch_inline_query_current_chat="")]]
+ )
+ game.next()
+ await c.send_message(
+ game.chat.id, ct("next").format(game.next_player.mention), reply_markup=inline_keyb
+ )
+ del game.players[m.from_user.id]
+ del player_game[m.from_user.id]
+ chat_id = m.chat.id if isinstance(m, Message) else m.message.chat.id
+
+ if len(game.players) == 0:
+ games.pop(m.chat.id if isinstance(m, Message) else m.message.chat.id)
+ if isinstance(m, CallbackQuery):
+ await c.send_message(
+ chat_id=chat_id,
+ text=ct("player_left").format(m.from_user.mention)+", "+ct("game_over"),
+ )
+ return await func(ut("game_over"))
+ if isinstance(m, CallbackQuery):
+ await c.send_message(
+ chat_id=chat_id, text=ct("player_left").format(m.from_user.mention)
+ )
+ return await func(ut("left_game"))
+
+
+@Client.on_message(filters.command("close"))
+@use_lang()
+async def close_game(c: Client, m: Message, ut, ct):
+ game = games.get(m.chat.id)
+ if not game or m.from_user != list(game.players.values())[0]:
+ return await m.reply_text(
+ ut("no_game")
+ if not game
+ else ut("not_allowed")
+ )
+
+ games.closed = True
+ return await m.reply_text(ct("lobby_closed"))
+
+
+@Client.on_message(filters.command("open"))
+@use_lang()
+async def open_game(c: Client, m: Message, ut, ct):
+ """Handles the opening of a game."""
+ game = games.get(m.chat.id)
+ if not game or m.from_user != list(game.players.values())[0]:
+ return await m.reply_text(
+ ut("no_game")
+ if not game
+ else ut("not_allowed")
+ )
+
+ games.closed = False
+ return await m.reply_text(ct("lobby_opened"))
+
+
+@Client.on_message(filters.command("kill"))
+@use_lang()
+async def kill_game(c: Client, m: Message, ut, ct):
+ game = games.get(m.chat.id)
+ if not game or m.from_user != list(game.players.values())[0]:
+ return await m.reply_text(
+ ut("no_game")
+ if not game
+ else ut("not_allowed")
+ )
+
+ games.pop(m.chat.id)
+ for player in game.players:
+ player_game.pop(player)
+ return await m.reply_text(ct("game_over"))
+
+
+@Client.on_message(filters.command("start") & ~filters.private)
+@Client.on_callback_query(filters.regex("^start_game$"))
+@use_lang()
+async def start_game(c: Client, m: Union[Message, CallbackQuery], ut, ct):
+ if isinstance(m, CallbackQuery):
+ func = m.answer
+ chat_id = m.message.chat.id
+ else:
+ func = m.reply_text
+ chat_id = m.chat.id
+ config = await Chat.get(id=chat_id)
+ theme = config.theme
+ game = games.get(chat_id)
+ if not game or m.from_user != list(game.players.values())[0]:
+ return await func(
+ ut("no_game")
+ if not game
+ else ut("not_allowed")
+ )
+
+ game.is_started = True
+ game.deck.shuffle()
+ for player in game.players.values():
+ player.cards = game.deck.draw(7)
+ player.total_cards = 0
+
+ await c.send_message(chat_id=chat_id, text=ct("game_started"))
+ pcard = next(
+ (
+ lcard
+ for lcard in game.deck.cards
+ if lcard[1] not in cards[config.theme]["CARDS"]["SPECIALS"]
+ ),
+ None,
+ )
+ game.deck.cards.remove(pcard)
+ game.deck.cards.append(pcard)
+ if pcard[1] == "draw":
+ if game.draw == -1:
+ game.draw = 0
+ game.draw += 2
+ await c.send_sticker(
+ chat_id=chat_id, sticker=cards[theme]["STICKERS"][f"{pcard[0]}_{pcard[1]}"]
+ )
+ game.last_card = pcard
+ game.next_player = list(game.players.values())[0]
+ inline_keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(ct("play"), switch_inline_query_current_chat="")]]
+ )
+ return await c.send_message(
+ chat_id,
+ ct("next").format(game.next_player.mention),
+ reply_markup=inline_keyb,
+ )
+
+
+@Client.on_inline_query(group=3)
+@use_lang()
+async def inline_query(c: Client, m: InlineQuery, ut, ct):
+ game = player_game.get(m.from_user.id)
+ if not game:
+ articles = [
+ InlineQueryResultArticle(
+ id="none",
+ title=ut("no_game"),
+ input_message_content=InputTextMessageContent(
+ ut("no_game_text")
+ ),
+ )
+ ]
+ return await m.answer(articles, cache_time=0)
+
+ config = await Chat.get(id=game.chat.id)
+ theme = config.theme
+ COLOR_ICONS = cards[theme]["CARDS"]["COLOR_ICONS"]
+ VALUES_ICONS = cards[theme]["CARDS"]["VALUES_ICONS"]
+ if game.chosen == "color" and game.next_player.id == m.from_user.id:
+ pre = ""
+ if game.last_card[1] in cards[theme]["CARDS"]["THEME_CARDS"]:
+ pre = f"{game.last_card[1]}-"
+ articles = [
+ InlineQueryResultArticle(
+ id=pre + color,
+ title=COLOR_ICONS[color],
+ input_message_content=InputTextMessageContent(
+ ct("colorchoosed").format(color=COLOR_ICONS[color])
+ ),
+ )
+ for color in COLORS
+ ]
+ await m.answer(articles, cache_time=0)
+ return
+ elif game.chosen == "player" and game.next_player.id == m.from_user.id:
+ players = list(game.players.keys())
+ players.remove(m.from_user.id)
+ articles = []
+ pre = ""
+ if game.last_card[1] in cards[theme]["CARDS"]["THEME_CARDS"]:
+ pre = f"{game.last_card[1]}-"
+ for player in players:
+ if len(players) == 1 or player != m.from_user.id:
+ articles.append(
+ InlineQueryResultArticle(
+ id=pre + str(player),
+ title=game.players[player].first_name,
+ input_message_content=InputTextMessageContent(
+ game.players[player].mention
+ ),
+ )
+ )
+ await m.answer(articles, cache_time=0)
+ return
+
+ info_text = ut("info_text").format(current_player=game.next_player.mention, last_card=COLOR_ICONS[game.last_card[0]]+VALUES_ICONS[game.last_card[1]])
+ for fplayer in game.players:
+ info_text += ut("info_text2").format(player=game.players[fplayer].mention, cards=len(game.players[fplayer].cards))
+
+ if not game or m.from_user.id != game.next_player.id:
+ articles = []
+ for num, pcard in enumerate(game.players[m.from_user.id].cards):
+ sticker_type = pcard[1] if pcard[0] == "x" else f"{pcard[0]}_{pcard[1]}"
+ articles += [
+ InlineQueryResultCachedSticker(
+ id=f"info-{num}",
+ sticker_file_id=cards[theme]["STICKERS_GREY"][sticker_type],
+ input_message_content=InputTextMessageContent(info_text),
+ )
+ ]
+
+ return await m.answer(articles, cache_time=0, is_gallery=True)
+
+ gcards = game.players[m.from_user.id].cards
+ lcard = game.last_card if game.draw < 2 else ("p", "draw")
+ xcard = f"{lcard[0]}_{lcard[1]}"
+ sticker_id = "option_pass" if game.draw == -1 else "option_draw"
+ sticker_text = (
+ ut("pass")
+ if game.draw == -1
+ else ut("buy").format(1)
+ if game.draw == 0
+ else ut("buy").format(game.draw)
+ )
+ articles = [
+ InlineQueryResultCachedSticker(
+ id=sticker_id,
+ sticker_file_id=cards[theme]["STICKERS"][sticker_id],
+ input_message_content=InputTextMessageContent(sticker_text),
+ )
+ ]
+
+ if game.last_card[1] == "draw_four" and game.draw == 4:
+ if (await Chat.get(id=game.chat.id)).bluff:
+ articles.append(
+ InlineQueryResultCachedSticker(
+ id="option_bluff",
+ sticker_file_id=cards[theme]["STICKERS"]["option_bluff"],
+ input_message_content=InputTextMessageContent(
+ ut("bluff")
+ ),
+ )
+ )
+
+ for num, pcard in enumerate(gcards):
+ sticker_type = pcard[1] if pcard[0] == "x" else f"{pcard[0]}_{pcard[1]}"
+ if (
+ (
+ pcard[1] in cards[theme]["CARDS"]["SPECIALS_INFO"]
+ and re.search(
+ cards[theme]["CARDS"]["SPECIALS_INFO"][pcard[1]][1], string=xcard
+ )
+ )
+ or pcard[0] == lcard[0]
+ or pcard[1] == lcard[1]
+ ):
+ articles.append(
+ InlineQueryResultCachedSticker(
+ id=f"{pcard[0]}-{pcard[1]}-{num}",
+ sticker_file_id=cards[theme]["STICKERS"][sticker_type],
+ )
+ )
+ else:
+ articles.append(
+ InlineQueryResultCachedSticker(
+ id=f"info-{num}",
+ sticker_file_id=cards[theme]["STICKERS_GREY"][sticker_type],
+ input_message_content=InputTextMessageContent(info_text),
+ )
+ )
+
+ await m.answer(articles, cache_time=0, is_gallery=True)
+
+
+@Client.on_chosen_inline_result(group=1)
+@use_lang()
+async def choosen(c: Client, ir: ChosenInlineResult, ut, ct):
+ game: Game = player_game.get(ir.from_user.id)
+ if not game and game.next_player.id != ir.from_user.id:
+ return
+
+ config = await Chat.get(id=game.chat.id)
+ pcard = ir.result_id.split("-")[:2]
+ ncard = ir.result_id.split("-")[2] if len(ir.result_id.split("-")) > 2 else None
+ lcard = game.last_card if game.draw <= 1 else ("y", "draw")
+ print(game.deck.cards)
+ inline_keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(ct("play"), switch_inline_query_current_chat="")]]
+ )
+ if pcard[0] == "info":
+ return
+ elif game.chosen == "color":
+ game.last_card = (ir.result_id, game.last_card[1])
+ game.chosen = None
+ elif game.chosen == "player" and game.last_card_2["card"][1] == "7":
+ game.players[ir.from_user.id].cards, game.players[int(pcard[0])].cards = (
+ game.players[int(pcard[0])].cards,
+ game.players[ir.from_user.id].cards,
+ )
+ game.chosen = None
+ await c.send_message(
+ game.chat.id,
+ ct("cards_swapped").format(name1=game.next_player.mention, name2=game.players[int(pcard[0])].mention),
+ )
+ game.next()
+ pcard.append("player")
+ await verify_cards(game, c, ir, game.players[int(pcard[0])], ut, ct)
+ elif pcard[0] == "option_draw":
+ buy = game.draw if game.draw > 0 else 1
+ game.players[ir.from_user.id].cards.extend(game.deck.draw(buy))
+ await c.send_message(game.chat.id, ct("bought").format(buy=buy))
+ game.draw = 0 if buy >= 2 else -1 if len(game.players) != 1 else 0
+ elif pcard[0] == "option_pass":
+ game.draw = 0
+ elif pcard[0] == "option_bluff":
+ lplayer = game.players[game.last_card_2["player"]]
+ bcard = game.last_card_2["card"]
+ if any(bcard[0] in tupla or bcard[1] in tupla for tupla in lplayer.cards):
+ lplayer.cards.extend(game.deck.draw(game.draw))
+ await c.send_message(
+ game.chat.id,
+ ct("bluffed").format(name=lplayer.mention, draw=game.draw),
+ )
+ game.draw = 0
+ await c.send_message(
+ game.chat.id,
+ ct("next").format(game.next_player.mention),
+ reply_markup=inline_keyb,
+ )
+ return
+ else:
+ game.players[ir.from_user.id].cards.extend(game.deck.draw(game.draw + 2))
+ await c.send_message(
+ game.chat.id,
+ ct("not_bluffed").format(name1=lplayer.mention, name2=ir.from_user.mention, draw=game.draw + 2),
+ )
+ game.draw = 0
+ game.next()
+ await c.send_message(
+ game.chat.id,
+ f"Próximo: {game.next_player.mention}",
+ reply_markup=inline_keyb,
+ )
+ return
+ elif pcard[0] == "x" and pcard[1] in ["colorchooser", "draw_four"]:
+ game.players[ir.from_user.id].total_cards += 1
+ if pcard[1] == "draw_four":
+ if game.draw == -1:
+ game.draw = 0
+ game.draw += 4
+ game.last_card_2 = {"card": game.last_card, "player": ir.from_user.id}
+ game.last_card = game.players[ir.from_user.id].cards.pop(int(ncard))
+ game.deck.cards.append(game.last_card)
+ game.chosen = "color"
+ return await c.send_message(
+ game.chat.id,
+ ct("colorchoose").format(name=game.next_player.mention),
+ reply_markup=inline_keyb,
+ )
+ elif (
+ pcard[0] in cards[config.theme]["CARDS"]["THEME_CARDS"]
+ or (pcard[1] and pcard[1] in cards[config.theme]["CARDS"]["THEME_CARDS"])
+ ):
+ game.deck.cards.append(game.last_card)
+ module = importlib.import_module(f"spetials.{config.theme}")
+ function = getattr(
+ module,
+ pcard[1]
+ if pcard[1] in cards[config.theme]["CARDS"]["THEME_CARDS"]
+ else pcard[0],
+ )
+ ret = await function(c, ir, game)
+ if ret:
+ return
+ elif pcard[0] != "x" and (pcard[1] == lcard[1] or pcard[0] == lcard[0]):
+ game.players[ir.from_user.id].total_cards += 1
+ game.draw += 2 if pcard[1] == "draw" else 0
+ game.last_card_2 = {"card": game.last_card, "player": ir.from_user.id}
+ game.last_card = game.players[ir.from_user.id].cards.pop(int(ncard))
+ game.deck.cards.append(game.last_card)
+ if pcard[1] == "reverse":
+ game.players = {
+ k: game.players[k] for k in reversed(list(game.players.keys()))
+ }
+ game.next() if len(game.players) == 2 else None
+ elif pcard[1] == "skip":
+ game.next()
+ elif ((await Chat.get(id=game.chat.id)).seven) and (
+ pcard[1] == "7" or pcard[1] == "0"
+ ):
+ if pcard[1] == "7":
+ if len(game.players) == 1 or len(game.players) == 2:
+ await c.send_message(
+ game.chat.id,
+ ct("not_swapped")
+ )
+ else:
+ game.chosen = "player"
+ game.last_card_2 = {
+ "card": game.last_card,
+ "player": ir.from_user.id,
+ }
+ return await c.send_message(
+ game.chat.id,
+ ct("playerchoose").format(name=game.next_player.mention),
+ reply_markup=inline_keyb,
+ )
+ else:
+ # Aqui irá trocar as cartas dos jogadores na ordem do jogo
+ gcards = {}
+ a = 0
+ for i in game.players:
+ gcards[a] = game.players[i].cards
+ a += 1
+
+ a = 1
+ for i in game.players:
+ if a in gcards:
+ game.players[i].cards = gcards[a]
+ else:
+ game.players[i].cards = gcards[0]
+ a += 1
+ await verify_cards(game, c, ir, game.players[i], ut, ct)
+
+ await c.send_message(game.chat.id, ct("swapped"))
+ if game.draw == -1:
+ game.draw = 0
+ else:
+ return await c.send_message(game.chat.id, ct("invalid_card"))
+
+ if games.get(game.chat.id):
+ await verify_cards(game, c, ir, ir.from_user, ut, ct)
+ game.next()
+ return await c.send_message(
+ game.chat.id, ct("next").format(game.next_player.mention), reply_markup=inline_keyb
+ )
+
+async def verify_cards(game, c, ir, user, ut, t):
+ inline_keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(t("play"), switch_inline_query_current_chat="")]]
+ )
+ if len(game.players[user.id].cards) == 1:
+ if not (await Chat.get(id=game.chat.id)).one_card:
+ await c.send_message(game.chat.id, t("said_uno").format(name=user.mention))
+ else:
+ await c.send_message(
+ game.chat.id,
+ ut("say_uno").format(name=user.mention),
+ )
+ uno = False
+ while True:
+ try:
+ cmessage = await game.chat.listen(
+ filters.text | filters.user(user.id), timeout=5
+ )
+ if (cmessage and cmessage.text) and "uno" in cmessage.text.lower():
+ uno = True
+ break
+ except ListenerTimeout:
+ break
+ if uno:
+ await c.send_message(
+ game.chat.id, t("said_uno").format(name=user.mention)
+ )
+ else:
+ game.players[user.id].cards.extend(game.deck.draw(2))
+ await c.send_message(
+ game.chat.id,
+ t("not_said_uno").format(name=user.mention),
+ )
+
+ elif len(game.players[user.id].cards) == 0:
+ comp_text = t("first") if game.winner else ""
+ await c.send_message(game.chat.id, t("won").format(name=user.mention, comp=comp_text))
+ if game.winner:
+ game.winner = False
+ db_user = await User.get_or_none(id=user.id)
+ if db_user and db_user.placar:
+ db_user.wins += 1
+ await db_user.save()
+ if (await Chat.get(id=game.chat.id)).one_win and len(game.players) > 2:
+ db_user = await User.get_or_none(id=user.id)
+ if db_user and db_user.placar:
+ db_user.matches += 1
+ db_user.cards += game.players[user.id].total_cards
+ await db_user.save()
+ game.next()
+ game.players.pop(user.id)
+ player_game.pop(user.id)
+ await c.send_message(
+ game.chat.id, t("continuing").format(count=len(game.players))
+ )
+ await c.send_message(
+ game.chat.id,
+ t("next").format(game.next_player.mention),
+ reply_markup=inline_keyb,
+ )
+ else:
+ games.pop(game.chat.id)
+ for player in game.players:
+ db_user = await User.get_or_none(id=player)
+ if db_user and db_user.placar:
+ db_user.matches += 1
+ db_user.cards += game.players[player].total_cards
+ await db_user.save()
+ player_game.pop(player)
+ await c.send_message(game.chat.id, t("game_over"))
+ return
diff --git a/plugins/settings.py b/plugins/settings.py
new file mode 100644
index 0000000..09ad5e9
--- /dev/null
+++ b/plugins/settings.py
@@ -0,0 +1,209 @@
+from hydrogram import Client, filters
+from hydrogram.types import Message, CallbackQuery
+from hydrogram.enums import ChatType, ChatMemberStatus
+from hydrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from typing import Union
+from functools import partial
+from card import cards
+from db import Chat, User
+from config import games
+from locales import langs, get_locale_string, use_chat_lang, use_user_lang
+from hydrogram.helpers import ikb
+
+
+@Client.on_callback_query(filters.regex("^settings$"))
+@Client.on_message(filters.command("settings"))
+@use_chat_lang()
+async def settings(c: Client, m: Union[Message, CallbackQuery], t):
+ if (
+ isinstance(m, Message)
+ and m.chat.type == ChatType.PRIVATE
+ or isinstance(m, CallbackQuery)
+ and m.message.chat.type == ChatType.PRIVATE
+ ):
+ print(m)
+ if isinstance(m, Message):
+ func = m.reply_text
+ else:
+ func = m.edit_message_text
+
+ x = await User.get(id=m.from_user.id)
+ keyb = [
+ [(t("language"), "info_lang"), (t("lang_flag"), "lang")],
+ [(t("status"), "status"), ("✅" if x.placar else "✖️", "ch_status")],
+ ]
+
+ if await filters.filter_sudoers(c, m):
+ keyb.append([("sudos", "sudos")])
+
+ keyb.append([(t("back"), "start")])
+
+ await func(t("settings"), reply_markup=ikb(keyb))
+ else:
+ chat_id = m.chat.id if isinstance(m, Message) else m.message.chat.id
+ admin = await c.get_chat_member(chat_id, m.from_user.id)
+ print(admin)
+ if admin.status == ChatMemberStatus.MEMBER:
+ await m.reply("You need to be an admin to change the settings!")
+ return
+ if games.get(chat_id) and games[chat_id].is_started:
+ await m.reply("You can't change the settings while a game is running!")
+ return
+ x = (await Chat.get_or_create(id=chat_id))[0]
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(t("theme"), callback_data="info_theme"),
+ InlineKeyboardButton(t(x.theme), callback_data="theme"),
+ ],
+ [
+ InlineKeyboardButton(t("seven_zero"), callback_data="info_seven"),
+ InlineKeyboardButton(
+ "✅" if x.seven else "✖️", callback_data="mode_seven"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("sbluff"), callback_data="info_bluff"),
+ InlineKeyboardButton(
+ "✅" if x.bluff else "✖️", callback_data="mode_bluff"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("one_win"), callback_data="info_one_win"),
+ InlineKeyboardButton(
+ "✅" if x.one_win else "✖️", callback_data="mode_one_win"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("one_card"), callback_data="info_one_card"),
+ InlineKeyboardButton(
+ "✅" if x.one_card else "✖️", callback_data="mode_one_card"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("language"), callback_data="info_lang"),
+ InlineKeyboardButton(t("lang_flag"), callback_data="lang"),
+ ],
+ ]
+ )
+ if isinstance(m, Message):
+ await m.reply_text(t("settings"), reply_markup=keyb)
+ else:
+ await m.edit_message_text(t("settings"), reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("^theme"))
+@use_chat_lang()
+async def theme(c: Client, cq: CallbackQuery, t):
+ if " " in cq.data:
+ await Chat.get(id=cq.message.chat.id).update(theme=cq.data.split(" ")[1])
+ keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(t("back"), callback_data="theme")]]
+ )
+ await cq.message.edit_text("Theme changed!", reply_markup=keyb)
+ else:
+ themes = cards.keys()
+ tkeyb = [
+ InlineKeyboardButton(text=t(theme), callback_data=f"theme {theme}")
+ for theme in themes
+ ]
+ keyb = InlineKeyboardMarkup(
+ [tkeyb, [InlineKeyboardButton(text=t("back"), callback_data="settings")]]
+ )
+ await cq.message.edit_text(t("theme_config"), reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("mode_"))
+@use_chat_lang()
+async def mode(c: Client, cq: CallbackQuery, t):
+ admin = await c.get_chat_member(cq.message.chat.id, cq.from_user.id)
+ if admin.status is ChatMemberStatus.MEMBER:
+ await cq.answer("You need to be an admin to change the settings!")
+ return
+ x = await Chat.get(id=cq.message.chat.id)
+ if cq.data == "mode_seven":
+ await Chat.get(id=cq.message.chat.id).update(seven=not x.seven)
+ elif cq.data == "mode_bluff":
+ await Chat.get(id=cq.message.chat.id).update(bluff=not x.bluff)
+ elif cq.data == "mode_one_win":
+ await Chat.get(id=cq.message.chat.id).update(one_win=not x.one_win)
+ elif cq.data == "mode_one_card":
+ await Chat.get(id=cq.message.chat.id).update(one_card=not x.one_card)
+ await cq.answer("Done!")
+ x = await Chat.get(id=cq.message.chat.id)
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(t("theme"), callback_data="info_theme"),
+ InlineKeyboardButton(t(x.theme), callback_data="theme"),
+ ],
+ [
+ InlineKeyboardButton(t("seven_zero"), callback_data="info_seven"),
+ InlineKeyboardButton(
+ "✅" if x.seven else "✖️", callback_data="mode_seven"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("sbluff"), callback_data="info_bluff"),
+ InlineKeyboardButton(
+ "✅" if x.bluff else "✖️", callback_data="mode_bluff"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("one_win"), callback_data="info_one_win"),
+ InlineKeyboardButton(
+ "✅" if x.one_win else "✖️", callback_data="mode_one_win"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("one_card"), callback_data="info_one_card"),
+ InlineKeyboardButton(
+ "✅" if x.one_card else "✖️", callback_data="mode_one_card"
+ ),
+ ],
+ [
+ InlineKeyboardButton(t("language"), callback_data="info_lang"),
+ InlineKeyboardButton(t("lang_flag"), callback_data="lang"),
+ ],
+ ]
+ )
+ await cq.message.edit_text(t("settings"), reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("^lang"))
+@use_chat_lang()
+async def lang(c: Client, cq: CallbackQuery, t):
+ if cq.message.chat.type != ChatType.PRIVATE:
+ admin = await c.get_chat_member(cq.message.chat.id, cq.from_user.id)
+ if admin.status is ChatMemberStatus.MEMBER:
+ await cq.answer("You need to be an admin to change the theme!")
+ return
+ if "_" in cq.data:
+ nt = partial(get_locale_string, cq.data.split("_")[1])
+ if cq.message.chat.type == ChatType.PRIVATE:
+ await User.get(id=cq.message.chat.id).update(lang=cq.data.split("_")[1])
+ else:
+ await Chat.get(id=cq.message.chat.id).update(lang=cq.data.split("_")[1])
+ keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(nt("back"), callback_data="settings")]]
+ )
+ await cq.message.edit_text(nt("lang_changed"), reply_markup=keyb)
+ else:
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(
+ get_locale_string(lang, "lang_name"),
+ callback_data=f"lang_{lang}",
+ )
+ ]
+ for lang in langs
+ ]
+ )
+ await cq.message.edit_text(t("choose_lang"), reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("^info_"))
+@use_user_lang()
+async def info(c: Client, cq: CallbackQuery, t):
+ await cq.answer(t(cq.data), show_alert=True)
diff --git a/plugins/start.py b/plugins/start.py
new file mode 100644
index 0000000..4dfb28c
--- /dev/null
+++ b/plugins/start.py
@@ -0,0 +1,68 @@
+from hydrogram import Client, filters
+from hydrogram.types import Message, CallbackQuery
+from hydrogram.helpers import ikb
+from typing import Union
+from db import User
+from locales import use_user_lang
+
+
+@Client.on_message(filters.command("start") & filters.private)
+@Client.on_callback_query(filters.regex("^start$"))
+@use_user_lang()
+async def start(c: Client, m: Union[Message, CallbackQuery], t):
+ if isinstance(m, CallbackQuery):
+ func = m.edit_message_text
+ else:
+ func = m.reply_text
+ await User.get_or_create(id=m.from_user.id)
+ print(m.from_user.id)
+ keyb = [[("help", "help"), ("configurações", "settings")]]
+ await func(t("start_text"), reply_markup=ikb(keyb))
+
+
+@Client.on_callback_query(filters.regex("^help$"))
+@use_user_lang()
+async def help(c: Client, cq: CallbackQuery, t):
+ keyb = [
+ [(t("game_mode"), "help_game")],
+ [(t("back"), "start")],
+ ]
+ await cq.edit_message_text(
+ "Ecolha uma opção de ajuda abaixo:", reply_markup=ikb(keyb)
+ )
+
+
+@Client.on_callback_query(filters.regex("^help_game$"))
+@use_user_lang()
+async def help_game(c: Client, cq: CallbackQuery, t):
+ text = t("game_mode_text")
+ text += "" + t("seven_zero") + ": " + t("info_seven") + "\n\n"
+ text += "" + t("sbluff") + ": " + t("info_bluff") + "\n\n"
+ text += "" + t("one_win") + ": " + t("info_one_win") + "\n\n"
+ text += "" + t("one_card") + ": " + t("info_one_card") + "\n\n"
+ keyb = [[(t("back"), "help")]]
+
+ await cq.edit_message_text(text, reply_markup=ikb(keyb))
+
+
+@Client.on_message(filters.command("status"))
+@Client.on_callback_query(filters.regex("^status$"))
+async def status(c: Client, m: Union[Message, CallbackQuery]):
+ user = await User.get_or_create(id=m.from_user.id)
+ porcentagem = (user[0].wins / user[0].matches) * 100 if user[0].matches > 0 else 0
+ if (await User.get(id=m.from_user.id)).placar:
+ text = f"{user[0].matches} partidas jogadas\n{user[0].wins} vezes em primeiro lugas ({porcentagem}%)\n{user[0].cards} cartas jogadas"
+ else:
+ text = "Ligue para saber suas estatísticas como quantas partidas jogadas, quantas vezes em primeiro lugar e quantas cartas jogadas"
+ if isinstance(m, Message):
+ await m.reply_text(text)
+ else:
+ await m.answer(text, show_alert=True)
+
+@Client.on_callback_query(filters.regex("^ch_status$"))
+async def ch_status(c: Client, cq: CallbackQuery):
+ user = await User.get_or_create(id=cq.from_user.id)
+ user = user[0]
+ user.placar = not user.placar
+ await user.save()
+ await cq.answer("Status alterado com sucesso")
diff --git a/plugins/sudos.py b/plugins/sudos.py
new file mode 100644
index 0000000..b3f72cd
--- /dev/null
+++ b/plugins/sudos.py
@@ -0,0 +1,467 @@
+from asyncio import sleep
+from json import dump
+from typing import Union
+
+from hydrogram import Client, filters
+from hydrogram.errors import ListenerTimeout, MediaEmpty
+from hydrogram.types import (
+ CallbackQuery,
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ InlineQuery,
+ InlineQueryResultCachedSticker,
+ Message,
+)
+
+from card import cards
+from config import sudoers
+from db import User
+
+
+async def filter_sudoers_logic(flt, c, u):
+ if not u.from_user:
+ return None
+ usr = u.from_user
+ db_usr = await User.get_or_none(id=usr.id)
+ if not db_usr:
+ return False
+ if db_usr.sudo or usr.id in sudoers:
+ return True
+ else:
+ return False
+
+
+filter_sudoers = filters.create(filter_sudoers_logic, "FilterSudoers")
+filters.filter_sudoers = filter_sudoers
+
+
+@Client.on_message(filters.command("sudos") & filters.private & filter_sudoers)
+@Client.on_callback_query(filters.regex("^sudos$") & filter_sudoers)
+async def start(c: Client, m: Union[Message, CallbackQuery]):
+ if isinstance(m, CallbackQuery):
+ func = m.edit_message_text
+ else:
+ func = m.reply_text
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton("Sudos", callback_data="settings_sudos"),
+ InlineKeyboardButton("themes", callback_data="settings_sudo_themc"),
+ ],
+ [
+ InlineKeyboardButton("Voltar", callback_data="settings"),
+ ]
+ ]
+ )
+ await func(
+ "Bem vindo ao menu de configurações do UnuRobot, aqui você pode configurar o bot da forma que quiser, para isso basta clicar em um dos botões abaixo!",
+ reply_markup=keyb,
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudos$") & filter_sudoers)
+async def settings_sudos(c: Client, cq: CallbackQuery):
+ usrs = await c.get_users(sudoers)
+ db_usrs = await User.filter(sudo=True)
+ usrs += await c.get_users([usr.id for usr in db_usrs])
+ text = "Lista de sudos:\n\n"
+ for usr in usrs:
+ text += f"👤 {usr.mention}\n"
+
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton("Adicionar", callback_data="settings_sudos_add"),
+ InlineKeyboardButton("Remover", callback_data="settings_sudos_remove"),
+ ],
+ [InlineKeyboardButton("Voltar", callback_data="sudos")],
+ ]
+ )
+
+ await cq.edit_message_text(text, reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("^settings_sudos_add$") & filter_sudoers)
+async def settings_sudos_add(c: Client, cq: CallbackQuery):
+ await cq.edit_message_text("Envie o ID do usuário que deseja adicionar aos sudos")
+ cmessage = None
+ # Wait for the user to send a message with the new emoji
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.text)
+ print(cmessage)
+ except ListenerTimeout:
+ return
+
+ text = cmessage.text
+ user = await c.get_users(text)
+
+ if not user:
+ await cmessage.reply_text("Usuário não encontrado")
+ return
+
+ user_db = await User.get_or_create(id=user.id)
+
+ if user_db[0].sudo:
+ await cmessage.reply_text("Usuário já é sudo")
+ return
+
+ user_db[0].sudo = True
+ await user_db[0].save()
+
+ await cmessage.reply_text(
+ "Usuário adicionado aos sudos",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Voltar", callback_data="settings_sudos")]]
+ ),
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudos_remove") & filter_sudoers)
+async def settings_sudos_remove(c: Client, cq: CallbackQuery):
+ if cq.data == "settings_sudos_remove":
+ db_users = await User.filter(sudo=True)
+ users = await c.get_users([usr.id for usr in db_users])
+ text = "Selecione o usuário que deseja remover dos sudos:\n\n"
+ keyb = []
+ for user in users:
+ keyb.append(
+ [
+ InlineKeyboardButton(
+ user.first_name,
+ callback_data=f"settings_sudos_remove_{user.id}",
+ )
+ ]
+ )
+
+ keyb.append([InlineKeyboardButton("Voltar", callback_data="settings_sudos")])
+ await cq.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyb))
+ return
+
+ db_users = await User.filter(sudo=True)
+ sudoers = [usr.id for usr in db_users]
+
+ user_id = int(cq.data.split("_")[3])
+ if user_id not in sudoers:
+ await cq.answer("Usuário não é sudo")
+ return
+
+ await User.get(id=user_id).update(sudo=False)
+
+ await cq.edit_message_text(
+ "Usuário removido dos sudos",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Voltar", callback_data="settings_sudos")]]
+ ),
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudo_themc$") & filter_sudoers)
+async def settings_sudo_themc(c: Client, cq: CallbackQuery):
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(
+ text=theme, callback_data=f"settings_sudo_themc {theme}"
+ )
+ for theme in cards.keys()
+ ],
+ [
+ InlineKeyboardButton(
+ "Adicionar novo", callback_data="settings_sudo_themc_new"
+ ),
+ InlineKeyboardButton("Voltar", callback_data="sudos"),
+ ],
+ ]
+ )
+ await cq.edit_message_text("Selecione um tema:", reply_markup=keyb)
+
+
+@Client.on_callback_query(filters.regex("^settings_sudo_themc_new$") & filter_sudoers)
+async def settings_themes_new(c: Client, cq: CallbackQuery):
+ await cq.edit_message_text("Envie o nome do tema")
+ cmessage = None
+ # Wait for the user to send a message with the new emoji
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.text)
+ print(cmessage)
+ except ListenerTimeout:
+ return
+
+ name = cmessage.text
+
+ ncards = cards["classic"]
+
+ for card in ncards["STICKERS"]:
+ await c.send_message(
+ cq.message.chat.id, "Mande um sticker para substituir o sticker:"
+ )
+ await c.send_sticker(cq.message.chat.id, ncards["STICKERS"][card])
+
+ cmessage = None
+ # Wait for the user to send a message with the new emoji
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ ncards["STICKERS"][card] = cmessage.sticker.file_id
+ except ListenerTimeout:
+ return
+ for card in ncards["STICKERS_GREY"]:
+ await c.send_message(
+ cq.message.chat.id, "Mande um sticker para substituir o sticker:"
+ )
+ await c.send_sticker(cq.message.chat.id, ncards["STICKERS_GREY"][card])
+
+ cmessage = None
+ # Wait for the user to send a message with the new emoji
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ ncards["STICKERS_GREY"][card] = cmessage.sticker.file_id
+ except ListenerTimeout:
+ return
+
+ with open(f"cards/{name}.json", "w") as f:
+ dump(ncards, f)
+
+ await c.send_message(
+ cq.message.chat.id,
+ "Tema adicionado com sucesso!",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Voltar", callback_data="settings_sudo_themc")]]
+ ),
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudo_themc ") & filter_sudoers)
+async def settings_themes(c: Client, cq: CallbackQuery):
+ theme = cq.data.split(" ")[1]
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(
+ "Adicionar", callback_data="settings_sudo_themc_add " + theme
+ ),
+ InlineKeyboardButton(
+ "Atualizar", callback_data="settings_sudo_themc_update " + theme
+ ),
+ InlineKeyboardButton(
+ "Verificar", callback_data="settings_sudo_themc_check " + theme
+ ),
+ ],
+ [InlineKeyboardButton("Voltar", callback_data="settings_sudo_themc")],
+ ]
+ )
+ await cq.edit_message_text(
+ f"O que deseja fazer com o tema {theme}?", reply_markup=keyb
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudo_themc_add ") & filter_sudoers)
+async def settings_themes_add(c: Client, cq: CallbackQuery):
+ theme = cq.data.split(" ")[1]
+ await cq.edit_message_text("Envie o código da nova carta")
+
+ cmessage = None
+
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.text)
+ except ListenerTimeout:
+ return
+
+ code = cmessage.text
+
+ await c.send_message(cq.message.chat.id, f"Mande um sticker colorido para o {code}")
+
+ cmessage = None
+
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ except ListenerTimeout:
+ return
+
+ stickerc = cmessage.sticker.file_id
+
+ await c.send_message(cq.message.chat.id, f"Mande um sticker cinza para o {code}")
+
+ cmessage = None
+
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ except ListenerTimeout:
+ return
+
+ stickerg = cmessage.sticker.file_id
+
+ cards[theme]["STICKERS"][code] = stickerc
+ cards[theme]["STICKERS_GREY"][code] = stickerg
+
+ with open(f"cards/{theme}.json", "w") as f:
+ dump(cards[theme], f)
+
+ await c.send_message(
+ cq.message.chat.id,
+ "Carta adicionada com sucesso!",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Voltar", callback_data="settings_sudo_themc")]]
+ ),
+ )
+
+
+@Client.on_callback_query(
+ filters.regex("^settings_sudo_themc_update ") & filter_sudoers
+)
+async def settings_themes_update(c: Client, cq: CallbackQuery):
+ sp = cq.data.split(" ")[1:]
+ print(sp)
+ theme = sp[0]
+ if len(sp) == 1:
+ keyb = InlineKeyboardMarkup(
+ [
+ [
+ InlineKeyboardButton(
+ "Light",
+ callback_data=f"settings_sudo_themc_update {theme} Light",
+ ),
+ InlineKeyboardButton(
+ "Dark", callback_data=f"settings_sudo_themc_update {theme} Dark"
+ ),
+ ],
+ ]
+ )
+ await cq.edit_message_text(
+ "Escolha o tema que deseja atualizar", reply_markup=keyb
+ )
+ if len(sp) == 2:
+ keyb = []
+ for color in cards[theme]["CARDS"]["COLORS"]:
+ keyb.append(
+ InlineKeyboardButton(
+ color,
+ callback_data=f"settings_sudo_themc_update {theme} {sp[1]} {color}",
+ )
+ )
+ keyb.append(
+ InlineKeyboardButton(
+ "SPECIALS",
+ callback_data=f"settings_sudo_themc_update {theme} {sp[1]} SPECIALS",
+ )
+ )
+ await cq.edit_message_text(
+ "Escolha a cor da carta que deseja atualizar",
+ reply_markup=InlineKeyboardMarkup([keyb]),
+ )
+ elif len(sp) == 3:
+ keyb = []
+ if sp[2] == "SPECIALS":
+ for card in cards[theme]["CARDS"]["SPECIALS"]:
+ keyb.append(
+ [
+ InlineKeyboardButton(
+ card,
+ callback_data=f"settings_sudo_themc_update {theme} {sp[1]} x {card}",
+ )
+ ]
+ )
+ else:
+ for card in cards[theme]["CARDS"]["VALUES"]:
+ keyb.append(
+ [
+ InlineKeyboardButton(
+ card,
+ callback_data=f"settings_sudo_themc_update {theme} {sp[1]} {sp[2]} {card}",
+ )
+ ]
+ )
+ await cq.edit_message_text(
+ "Escolha o valor da carta que deseja atualizar",
+ reply_markup=InlineKeyboardMarkup(keyb),
+ )
+ elif len(sp) == 4:
+ await cq.edit_message_text("Envie um sticker para substituir a carta:")
+ cardid = sp[2] + "_" + sp[3] if sp[2] != "x" else sp[3]
+ cardtm = "STICKERS_GREY" if sp[1] == "Dark" else "STICKERS"
+ await c.send_sticker(cq.message.chat.id, cards[theme][cardtm][cardid])
+
+ cmessage = None
+
+ while cmessage is None:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ except ListenerTimeout:
+ return
+
+ cards[theme][cardtm][cardid] = cmessage.sticker.file_id
+
+ with open(f"cards/{theme}.json", "w") as f:
+ dump(cards[theme], f)
+
+ await c.send_message(
+ cq.message.chat.id,
+ "Carta atualizada com sucesso!",
+ reply_markup=InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Voltar", callback_data="settings_sudo_themc")]]
+ ),
+ )
+
+
+@Client.on_callback_query(filters.regex("^settings_sudo_themc_check ") & filter_sudoers)
+async def settings_themes_check(c: Client, cq: CallbackQuery):
+ theme = cq.data.split(" ")[1]
+ card_types = ["STICKERS", "STICKERS_GREY"]
+ messages = ["Verificando cartas coloridas...", "Verificando cartas cinzas..."]
+
+ for card_type, message in zip(card_types, messages):
+ await c.send_message(cq.message.chat.id, message)
+ for card in cards[theme][card_type]:
+ try:
+ m2 = await c.send_sticker(
+ cq.message.chat.id, cards[theme][card_type][card]
+ )
+ await m2.delete()
+ await sleep(0.3)
+ except MediaEmpty:
+ await c.send_message(
+ cq.message.chat.id,
+ f"Carta {card} não encontrada, mande a carta para substitui-lá",
+ )
+
+ while True:
+ try:
+ cmessage = await cq.message.chat.listen(filters.sticker)
+ break
+ except ListenerTimeout:
+ return
+
+ cards[theme][card_type][card] = cmessage.sticker.file_id
+ await c.send_message(
+ cq.message.chat.id, f"Carta {card} atualizada com sucesso!"
+ )
+
+ with open(f"cards/{theme}.json", "w") as f:
+ dump(cards[theme], f)
+
+ await c.send_message(cq.message.chat.id, "Cartas verificadas com sucesso!")
+
+
+@Client.on_inline_query(filters.regex("^change ") & filter_sudoers)
+async def change(c: Client, iq: InlineQuery):
+ theme = iq.query.split(" ")[1]
+ articles = []
+ for card in cards[theme]["STICKERS"]:
+ articles.append(
+ InlineQueryResultCachedSticker(
+ id="sudo" + card, sticker_file_id=cards[theme]["STICKERS"][card]
+ )
+ )
+ for card in cards[theme]["STICKERS_GREY"]:
+ articles.append(
+ InlineQueryResultCachedSticker(
+ id="sudog" + card, sticker_file_id=cards[theme]["STICKERS_GREY"][card]
+ )
+ )
+ return await iq.answer(articles, cache_time=0)
diff --git a/spetials/minimalist.py b/spetials/minimalist.py
new file mode 100644
index 0000000..a06af66
--- /dev/null
+++ b/spetials/minimalist.py
@@ -0,0 +1,32 @@
+from hydrogram import Client
+from hydrogram.types import ChosenInlineResult
+from hydrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
+from game import Game
+
+inline_keyb = InlineKeyboardMarkup(
+ [[InlineKeyboardButton("Jogar", switch_inline_query_current_chat="")]]
+)
+
+
+async def mirror(c: Client, ir: ChosenInlineResult, game: Game):
+ if game.draw < 2:
+ await c.send_message(game.chat.id, "Você não pode descartar essa carta agora!")
+ return True
+ else:
+ print(game.last_card_2, game.last_card)
+ game.players[ir.from_user.id].total_cards += 1
+ ncard = ir.result_id.split("-")[2]
+ player = game.last_card_2["player"]
+ player = game.players[int(player)]
+ player.cards.extend(game.deck.draw(game.draw))
+ await c.send_message(
+ game.chat.id, f"{player.mention} comprou {game.draw} cartas!"
+ )
+ game.players[ir.from_user.id].cards.pop(int(ncard))
+ await c.send_message(
+ game.chat.id,
+ f"Próximo: {game.next_player.mention}",
+ reply_markup=inline_keyb,
+ )
+ game.draw = 0
+ return True
diff --git a/version.py b/version.py
new file mode 100644
index 0000000..9f715b1
--- /dev/null
+++ b/version.py
@@ -0,0 +1,13 @@
+version = "1.0.0-beta"
+
+ascii_art = f""" ___ ___ ___
+ /\\__\\ /\\__\\ /\\__\\
+ /:/ _/_ /:| _|_ /:/ _/_
+ /:/_/\\__\\ /::|/\\__\\ /:/_/\\__\\
+ \\:\\/:/ / \\/|::/ / \\:\\/:/ /
+ \\::/ / |:/ / \\::/ / _ _ |_ _ |_
+ \\/__/ \\/__/ \\/__/ | (_)|_)(_)|_
+
+ Version: {version} - Powered by Hydrogram
+
+"""