├── Constants ├── __init__.py ├── Config_example.py └── Cards.py ├── Boardgamebox ├── __init__.py ├── Player.py ├── State.py ├── Game.py └── Board.py ├── GamesController.py ├── requirements.txt ├── README.md ├── tests └── test_Commands.py ├── Commands.py └── MainController.py /Constants/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Boardgamebox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GamesController.py: -------------------------------------------------------------------------------- 1 | def init(): 2 | global games 3 | games = {} 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-telegram-bot>=8.0.0 2 | requests==2.12.4 3 | six==1.10.0 4 | ptbtest>=1.3 -------------------------------------------------------------------------------- /Constants/Config_example.py: -------------------------------------------------------------------------------- 1 | TOKEN = "YOURTOKEN" 2 | ADMIN = 000000000 #your telegram ID 3 | STATS = "../stats.json" 4 | -------------------------------------------------------------------------------- /Boardgamebox/Player.py: -------------------------------------------------------------------------------- 1 | class Player(object): 2 | def __init__(self, name, uid): 3 | self.name = name 4 | self.uid = uid 5 | self.role = None 6 | self.party = None 7 | self.is_dead = False 8 | self.inspected_players = {} 9 | -------------------------------------------------------------------------------- /Boardgamebox/State.py: -------------------------------------------------------------------------------- 1 | class State(object): 2 | """Storage object for game state""" 3 | def __init__(self): 4 | self.liberal_track = 0 5 | self.fascist_track = 0 6 | self.failed_votes = 0 7 | self.president = None 8 | self.nominated_president = None 9 | self.nominated_chancellor = None 10 | self.chosen_president = None 11 | self.chancellor = None 12 | self.dead = 0 13 | self.last_votes = {} 14 | self.game_endcode = 0 15 | self.drawn_policies = [] 16 | self.player_counter = 0 17 | self.veto_refused = False 18 | self.not_blues = [] 19 | -------------------------------------------------------------------------------- /Boardgamebox/Game.py: -------------------------------------------------------------------------------- 1 | from random import shuffle 2 | 3 | 4 | class Game(object): 5 | def __init__(self, cid, initiator): 6 | self.playerlist = {} 7 | self.player_sequence = [] 8 | self.cid = cid 9 | self.board = None 10 | self.initiator = initiator 11 | self.dateinitvote = None 12 | 13 | def add_player(self, uid, player): 14 | self.playerlist[uid] = player 15 | 16 | def get_blue(self): 17 | for uid in self.playerlist: 18 | if self.playerlist[uid].role == "Blue": 19 | return self.playerlist[uid] 20 | 21 | def get_fascists(self): 22 | fascists = [] 23 | for uid in self.playerlist: 24 | if self.playerlist[uid].role == "Fascist": 25 | fascists.append(self.playerlist[uid]) 26 | return fascists 27 | 28 | def shuffle_player_sequence(self): 29 | for uid in self.playerlist: 30 | self.player_sequence.append(self.playerlist[uid]) 31 | shuffle(self.player_sequence) 32 | 33 | def remove_from_player_sequence(self, Player): 34 | for p in self.player_sequence: 35 | if p.uid == Player.uid: 36 | p.remove(Player) 37 | 38 | def print_roles(self): 39 | rtext = "" 40 | if self.board is None: 41 | #game was not started yet 42 | return rtext 43 | else: 44 | for p in self.playerlist: 45 | rtext += self.playerlist[p].name + "'s " 46 | if self.playerlist[p].is_dead: 47 | rtext += "(dead) " 48 | rtext += "secret role was " + self.playerlist[p].role + "\n" 49 | return rtext 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Bot for the card game Secret Blue 2 | This is a simple Telegram Bot for the political (and in my opinion highly entertaining and educational) card game [Secret Hitler](http://secrethitler.com/). 3 | Go check out this awesome game and support the creators! 4 | 5 | ## Why the name change? 6 | The original game is called "Secret Hitler" and is meant as an entertaining but also educational game which can also be a warning about the takeover of fascist regimes. Since some people might only read the title and don't get the full idea behind this game I want to avoid any confusion about my intentions with this bot and decided to change the name. 7 | 8 | ## Start a game 9 | Unfortunately I don't have the time to to maintain and host the bot any longer. So if you want to play I suggest you start your own instance of the bot. [Here](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Hosting-your-bot) you can find information about hosting a telegram bot. 10 | 11 | ## The following commands are available: 12 | 13 | /help - Gives you information about the available commands 14 | 15 | /start - Gives you a short piece of information about Secret Blue 16 | 17 | /symbols - Shows you all possible symbols of the board 18 | 19 | /rules - Gives you a link to the official Secret Hitler rules 20 | 21 | /newgame - Creates a new game 22 | 23 | /join - Joins an existing game 24 | 25 | /startgame - Starts an existing game when all players have joined 26 | 27 | /cancelgame - Cancels an existing game. All data of the game will be lost 28 | 29 | /board - Prints the current board with fascist and liberals tracks, presidential order and election counter 30 | 31 | ## Copyright and licence 32 | Secret Hitler (© 2016 GOAT, WOLF, & CABBAGE) is designed by Max Temkin (Cards Against Humanity, Humans vs. Zombies) Mike Boxleiter (Solipskier, TouchTone), Tommy Maranges (Philosophy Bro) and illustrated by Mackenzie Schubert (Letter Tycoon, Penny Press). 33 | Secret Hitler is licensed under [Creative Commons BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) and so is this bot. 34 | -------------------------------------------------------------------------------- /Boardgamebox/Board.py: -------------------------------------------------------------------------------- 1 | from Constants.Cards import playerSets 2 | from Constants.Cards import policies 3 | import random 4 | from Boardgamebox.State import State 5 | 6 | 7 | class Board(object): 8 | def __init__(self, playercount, game): 9 | self.state = State() 10 | self.num_players = playercount 11 | self.fascist_track_actions = playerSets[self.num_players]["track"] 12 | self.policies = random.sample(policies, len(policies)) 13 | self.game = game 14 | self.discards = [] 15 | self.previous = [] 16 | 17 | def print_board(self): 18 | board = "--- Liberal acts ---\n" 19 | for i in range(5): 20 | if i < self.state.liberal_track: 21 | board += u"\u2716\uFE0F" + " " #X 22 | elif i >= self.state.liberal_track and i == 4: 23 | board += u"\U0001F54A" + " " #dove 24 | else: 25 | board += u"\u25FB\uFE0F" + " " #empty 26 | board += "\n--- Fascist acts ---\n" 27 | for i in range(6): 28 | if i < self.state.fascist_track: 29 | board += u"\u2716\uFE0F" + " " #X 30 | else: 31 | action = self.fascist_track_actions[i] 32 | if action == None: 33 | board += u"\u25FB\uFE0F" + " " # empty 34 | elif action == "policy": 35 | board += u"\U0001F52E" + " " # crystal 36 | elif action == "inspect": 37 | board += u"\U0001F50E" + " " # inspection glass 38 | elif action == "kill": 39 | board += u"\U0001F5E1" + " " # knife 40 | elif action == "win": 41 | board += u"\u2620" + " " # skull 42 | elif action == "choose": 43 | board += u"\U0001F454" + " " # tie 44 | 45 | board += "\n--- Election counter ---\n" 46 | for i in range(3): 47 | if i < self.state.failed_votes: 48 | board += u"\u2716\uFE0F" + " " #X 49 | else: 50 | board += u"\u25FB\uFE0F" + " " #empty 51 | 52 | board += "\n--- Presidential order ---\n" 53 | for player in self.game.player_sequence: 54 | board += player.name + " " + u"\u27A1\uFE0F" + " " 55 | board = board[:-3] 56 | board += u"\U0001F501" 57 | board += "\n\nThere are " + str(len(self.policies)) + " policies left on the pile." 58 | if self.state.fascist_track >= 3: 59 | board += "\n\n" + u"\u203C\uFE0F" + " Beware: If Blue gets elected as Chancellor the fascists win the game! " + u"\u203C\uFE0F" 60 | if len(self.state.not_blues) > 0: 61 | board += "\n\nWe know that the following players are not Blue because they got elected as Chancellor after 3 fascist policies:\n" 62 | for nh in self.state.not_blues: 63 | board += nh.name + ", " 64 | board = board[:-2] 65 | return board 66 | -------------------------------------------------------------------------------- /Constants/Cards.py: -------------------------------------------------------------------------------- 1 | playerSets = { 2 | # only for testing purposes 3 | 3: { 4 | "roles": [ 5 | "Liberal", 6 | "Fascist", 7 | "Blue" 8 | ], 9 | "track": [ 10 | None, 11 | None, 12 | "policy", 13 | "kill", 14 | "kill", 15 | "win" 16 | ] 17 | }, 18 | # only for testing purposes 19 | 4: { 20 | "roles": [ 21 | "Liberal", 22 | "Liberal", 23 | "Fascist", 24 | "Blue" 25 | ], 26 | "track": [ 27 | None, 28 | None, 29 | "policy", 30 | "kill", 31 | "kill", 32 | "win" 33 | ] 34 | }, 35 | 5: { 36 | "roles": [ 37 | "Liberal", 38 | "Liberal", 39 | "Liberal", 40 | "Fascist", 41 | "Blue" 42 | ], 43 | "track": [ 44 | None, 45 | None, 46 | "policy", 47 | "kill", 48 | "kill", 49 | "win" 50 | ] 51 | }, 52 | 6: { 53 | "roles": [ 54 | "Liberal", 55 | "Liberal", 56 | "Liberal", 57 | "Liberal", 58 | "Fascist", 59 | "Blue" 60 | ], 61 | "track": [ 62 | None, 63 | None, 64 | "policy", 65 | "kill", 66 | "kill", 67 | "win" 68 | ] 69 | }, 70 | 7: { 71 | "roles": [ 72 | "Liberal", 73 | "Liberal", 74 | "Liberal", 75 | "Liberal", 76 | "Fascist", 77 | "Fascist", 78 | "Blue" 79 | ], 80 | "track": [ 81 | None, 82 | "inspect", 83 | "choose", 84 | "kill", 85 | "kill", 86 | "win" 87 | ] 88 | }, 89 | 8: { 90 | "roles": [ 91 | "Liberal", 92 | "Liberal", 93 | "Liberal", 94 | "Liberal", 95 | "Liberal", 96 | "Fascist", 97 | "Fascist", 98 | "Blue" 99 | ], 100 | "track": [ 101 | None, 102 | "inspect", 103 | "choose", 104 | "kill", 105 | "kill", 106 | "win" 107 | ] 108 | }, 109 | 9: { 110 | "roles": [ 111 | "Liberal", 112 | "Liberal", 113 | "Liberal", 114 | "Liberal", 115 | "Liberal", 116 | "Fascist", 117 | "Fascist", 118 | "Fascist", 119 | "Blue" 120 | ], 121 | "track": [ 122 | "inspect", 123 | "inspect", 124 | "choose", 125 | "kill", 126 | "kill", 127 | "win" 128 | ] 129 | }, 130 | 10: { 131 | "roles": [ 132 | "Liberal", 133 | "Liberal", 134 | "Liberal", 135 | "Liberal", 136 | "Liberal", 137 | "Liberal", 138 | "Fascist", 139 | "Fascist", 140 | "Fascist", 141 | "Blue" 142 | ], 143 | "track": [ 144 | "inspect", 145 | "inspect", 146 | "choose", 147 | "kill", 148 | "kill", 149 | "win" 150 | ] 151 | }, 152 | } 153 | 154 | policies = [ 155 | "liberal", 156 | "liberal", 157 | "liberal", 158 | "liberal", 159 | "liberal", 160 | "liberal", 161 | "fascist", 162 | "fascist", 163 | "fascist", 164 | "fascist", 165 | "fascist", 166 | "fascist", 167 | "fascist", 168 | "fascist", 169 | "fascist", 170 | "fascist", 171 | "fascist" 172 | ] 173 | -------------------------------------------------------------------------------- /tests/test_Commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import unittest 3 | from Commands import * 4 | import GamesController 5 | from Boardgamebox.Board import Board 6 | from Boardgamebox.Game import Game 7 | from Boardgamebox.Player import Player 8 | 9 | from telegram.ext import CommandHandler 10 | from telegram.ext import Filters 11 | from telegram.ext import MessageHandler 12 | from telegram.ext import Updater 13 | 14 | from ptbtest import ChatGenerator 15 | from ptbtest import MessageGenerator 16 | from ptbtest import Mockbot 17 | from ptbtest import UserGenerator 18 | 19 | 20 | class TestCommands(unittest.TestCase): 21 | 22 | def setUp(self): 23 | # For use within the tests we nee some stuff. Starting with a Mockbot 24 | self.bot = Mockbot() 25 | # Some generators for users and chats 26 | self.ug = UserGenerator() 27 | self.cg = ChatGenerator() 28 | # And a Messagegenerator and updater (for use with the bot.) 29 | self.mg = MessageGenerator(self.bot) 30 | self.updater = Updater(bot=self.bot) 31 | GamesController.init() 32 | 33 | def test_ping(self): 34 | # Then register the handler with he updater's dispatcher and start polling 35 | self.updater.dispatcher.add_handler(CommandHandler("ping", command_ping)) 36 | self.updater.start_polling() 37 | # create with random user 38 | update = self.mg.get_message(text="/ping") 39 | # We insert the update with the bot so the updater can retrieve it. 40 | self.bot.insertUpdate(update) 41 | # sent_messages is the list with calls to the bot's outbound actions. Since we hope the message we inserted 42 | # only triggered one sendMessage action it's length should be 1. 43 | self.assertEqual(len(self.bot.sent_messages), 1) 44 | sent = self.bot.sent_messages[0] 45 | self.assertEqual(sent['method'], "sendMessage") 46 | self.assertEqual(sent['text'], "pong - v0.4") 47 | # Always stop the updater at the end of a testcase so it won't hang. 48 | self.updater.stop() 49 | 50 | def test_start(self): 51 | self.updater.dispatcher.add_handler(CommandHandler("start", command_start)) 52 | self.updater.start_polling() 53 | update = self.mg.get_message(text="/start") 54 | self.bot.insertUpdate(update) 55 | self.assertEqual(len(self.bot.sent_messages), 2) 56 | start = self.bot.sent_messages[0] 57 | self.assertEqual(start['method'], "sendMessage") 58 | self.assertIn("Secret Blue is a social deduction game", start['text']) 59 | help = self.bot.sent_messages[1] 60 | self.assertEqual(help['method'], "sendMessage") 61 | self.assertIn("The following commands are available", help['text']) 62 | self.updater.stop() 63 | 64 | def test_symbols(self): 65 | self.updater.dispatcher.add_handler(CommandHandler("symbols", command_symbols)) 66 | self.updater.start_polling() 67 | update = self.mg.get_message(text="/symbols") 68 | self.bot.insertUpdate(update) 69 | self.assertEqual(len(self.bot.sent_messages), 1) 70 | sent = self.bot.sent_messages[0] 71 | self.assertEqual(sent['method'], "sendMessage") 72 | self.assertIn("The following symbols can appear on the board:", sent['text']) 73 | self.updater.stop() 74 | 75 | def test_board_when_there_is_no_game(self): 76 | self.updater.dispatcher.add_handler(CommandHandler("board", command_board)) 77 | self.updater.start_polling() 78 | update = self.mg.get_message(text="/board") 79 | self.bot.insertUpdate(update) 80 | self.assertEqual(len(self.bot.sent_messages), 1) 81 | sent = self.bot.sent_messages[0] 82 | self.assertEqual(sent['method'], "sendMessage") 83 | self.assertIn("There is no game in this chat. Create a new game with /newgame", sent['text']) 84 | self.updater.stop() 85 | 86 | def test_board_when_game_is_not_running(self): 87 | game = Game(-999, 12345) 88 | GamesController.games[-999] = game 89 | self.updater.dispatcher.add_handler(CommandHandler("board", command_board)) 90 | self.updater.start_polling() 91 | chat = self.cg.get_chat(cid=-999) 92 | update = self.mg.get_message(chat=chat, text="/board") 93 | self.bot.insertUpdate(update) 94 | self.assertEqual(len(self.bot.sent_messages), 1) 95 | sent = self.bot.sent_messages[0] 96 | self.assertEqual(sent['method'], "sendMessage") 97 | self.assertIn("There is no running game in this chat. Please start the game with /startgame", sent['text']) 98 | self.updater.stop() 99 | 100 | def test_board_when_game_is_running(self): 101 | game = Game(-999, 12345) 102 | game.board = Board(5, game) 103 | GamesController.games[-999] = game 104 | self.updater.dispatcher.add_handler(CommandHandler("board", command_board)) 105 | self.updater.start_polling() 106 | chat = self.cg.get_chat(cid=-999) 107 | update = self.mg.get_message(chat=chat, text="/board") 108 | self.bot.insertUpdate(update) 109 | self.assertEqual(len(self.bot.sent_messages), 1) 110 | sent = self.bot.sent_messages[0] 111 | self.assertEqual(sent['method'], "sendMessage") 112 | self.assertIn("--- Liberal acts ---", sent['text']) 113 | self.updater.stop() 114 | -------------------------------------------------------------------------------- /Commands.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging as log 3 | 4 | import datetime 5 | 6 | 7 | 8 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode 9 | 10 | 11 | import MainController 12 | import GamesController 13 | from Constants.Config import STATS 14 | from Boardgamebox.Board import Board 15 | from Boardgamebox.Game import Game 16 | from Boardgamebox.Player import Player 17 | from Constants.Config import ADMIN 18 | 19 | # Enable logging 20 | log.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 21 | level=log.INFO, 22 | filename='logs/logging.log') 23 | 24 | logger = log.getLogger(__name__) 25 | 26 | commands = [ # command description used in the "help" command 27 | '/help - Gives you information about the available commands', 28 | '/start - Gives you a short piece of information about Secret Blue', 29 | '/symbols - Shows you all possible symbols of the board', 30 | '/rules - Gives you a link to the official Secret Blue rules', 31 | '/newgame - Creates a new game', 32 | '/join - Joins an existing game', 33 | '/startgame - Starts an existing game when all players have joined', 34 | '/cancelgame - Cancels an existing game. All data of the game will be lost', 35 | '/board - Prints the current board with fascist and liberals tracks, presidential order and election counter', 36 | '/votes - Prints who voted', 37 | '/calltovote - Calls the players to vote' 38 | ] 39 | 40 | symbols = [ 41 | u"\u25FB\uFE0F" + ' Empty field without special power', 42 | u"\u2716\uFE0F" + ' Field covered with a card', # X 43 | u"\U0001F52E" + ' Presidential Power: Policy Peek', # crystal 44 | u"\U0001F50E" + ' Presidential Power: Investigate Loyalty', # inspection glass 45 | u"\U0001F5E1" + ' Presidential Power: Execution', # knife 46 | u"\U0001F454" + ' Presidential Power: Call Special Election', # tie 47 | u"\U0001F54A" + ' Liberals win', # dove 48 | u"\u2620" + ' Fascists win' # skull 49 | ] 50 | 51 | 52 | def command_symbols(bot, update): 53 | cid = update.message.chat_id 54 | symbol_text = "The following symbols can appear on the board: \n" 55 | for i in symbols: 56 | symbol_text += i + "\n" 57 | bot.send_message(cid, symbol_text) 58 | 59 | 60 | def command_board(bot, update): 61 | cid = update.message.chat_id 62 | if cid in GamesController.games.keys(): 63 | if GamesController.games[cid].board: 64 | bot.send_message(cid, GamesController.games[cid].board.print_board()) 65 | else: 66 | bot.send_message(cid, "There is no running game in this chat. Please start the game with /startgame") 67 | else: 68 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 69 | 70 | 71 | def command_start(bot, update): 72 | cid = update.message.chat_id 73 | bot.send_message(cid, 74 | "\"Secret Blue is a social deduction game for 5-10 people about finding and stopping the Secret Blue." 75 | " The majority of players are liberals. If they can learn to trust each other, they have enough " 76 | "votes to control the table and win the game. But some players are fascists. They will say whatever " 77 | "it takes to get elected, enact their agenda, and blame others for the fallout. The liberals must " 78 | "work together to discover the truth before the fascists install their cold-blooded leader and win " 79 | "the game.\"\n- official description of Secret Blue\n\nAdd me to a group and type /newgame to create a game!") 80 | command_help(bot, update) 81 | 82 | 83 | def command_rules(bot, update): 84 | cid = update.message.chat_id 85 | btn = [[InlineKeyboardButton("Rules", url="http://www.secrethitler.com/assets/Secret_Hitler_Rules.pdf")]] 86 | rulesMarkup = InlineKeyboardMarkup(btn) 87 | bot.send_message(cid, "Read the official Secret Hitler rules:", reply_markup=rulesMarkup) 88 | 89 | 90 | # pings the bot 91 | def command_ping(bot, update): 92 | cid = update.message.chat_id 93 | bot.send_message(cid, 'pong - v0.4') 94 | 95 | 96 | # prints statistics, only ADMIN 97 | def command_stats(bot, update): 98 | cid = update.message.chat_id 99 | if cid == ADMIN: 100 | with open(STATS, 'r') as f: 101 | stats = json.load(f) 102 | stattext = "+++ Statistics +++\n" + \ 103 | "Liberal Wins (policies): " + str(stats.get("libwin_policies")) + "\n" + \ 104 | "Liberal Wins (killed Blue): " + str(stats.get("libwin_kill")) + "\n" + \ 105 | "Fascist Wins (policies): " + str(stats.get("fascwin_policies")) + "\n" + \ 106 | "Fascist Wins (Blue chancellor): " + str(stats.get("fascwin_blue")) + "\n" + \ 107 | "Games cancelled: " + str(stats.get("cancelled")) + "\n\n" + \ 108 | "Total amount of groups: " + str(len(stats.get("groups"))) + "\n" + \ 109 | "Games running right now: " 110 | bot.send_message(cid, stattext) 111 | 112 | 113 | # help page 114 | def command_help(bot, update): 115 | cid = update.message.chat_id 116 | help_text = "The following commands are available:\n" 117 | for i in commands: 118 | help_text += i + "\n" 119 | bot.send_message(cid, help_text) 120 | 121 | 122 | def command_newgame(bot, update): 123 | cid = update.message.chat_id 124 | game = GamesController.games.get(cid, None) 125 | groupType = update.message.chat.type 126 | if groupType not in ['group', 'supergroup']: 127 | bot.send_message(cid, "You have to add me to a group first and type /newgame there!") 128 | elif game: 129 | bot.send_message(cid, "There is currently a game running. If you want to end it please type /cancelgame!") 130 | else: 131 | GamesController.games[cid] = Game(cid, update.message.from_user.id) 132 | with open(STATS, 'r') as f: 133 | stats = json.load(f) 134 | if cid not in stats.get("groups"): 135 | stats.get("groups").append(cid) 136 | with open(STATS, 'w') as f: 137 | json.dump(stats, f) 138 | bot.send_message(cid, "New game created! Each player has to /join the game.\nThe initiator of this game (or the admin) can /join too and type /startgame when everyone has joined the game!") 139 | 140 | 141 | def command_join(bot, update): 142 | groupName = update.message.chat.title 143 | cid = update.message.chat_id 144 | groupType = update.message.chat.type 145 | game = GamesController.games.get(cid, None) 146 | fname = update.message.from_user.first_name 147 | 148 | if groupType not in ['group', 'supergroup']: 149 | bot.send_message(cid, "You have to add me to a group first and type /newgame there!") 150 | elif not game: 151 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 152 | elif game.board: 153 | bot.send_message(cid, "The game has started. Please wait for the next game!") 154 | elif update.message.from_user.id in game.playerlist: 155 | bot.send_message(game.cid, "You already joined the game, %s!" % fname) 156 | elif len(game.playerlist) >= 10: 157 | bot.send_message(game.cid, "You have reached the maximum amount of players. Please start the game with /startgame!") 158 | else: 159 | uid = update.message.from_user.id 160 | player = Player(fname, uid) 161 | try: 162 | bot.send_message(uid, "You joined a game in %s. I will soon tell you your secret role." % groupName) 163 | game.add_player(uid, player) 164 | except Exception: 165 | bot.send_message(game.cid, 166 | fname + ", I can\'t send you a private message. Please go to @thesecretbluebot and click \"Start\".\nYou then need to send /join again.") 167 | else: 168 | log.info("%s (%d) joined a game in %d" % (fname, uid, game.cid)) 169 | if len(game.playerlist) > 4: 170 | bot.send_message(game.cid, fname + " has joined the game. Type /startgame if this was the last player and you want to start with %d players!" % len(game.playerlist)) 171 | elif len(game.playerlist) == 1: 172 | bot.send_message(game.cid, "%s has joined the game. There is currently %d player in the game and you need 5-10 players." % (fname, len(game.playerlist))) 173 | else: 174 | bot.send_message(game.cid, "%s has joined the game. There are currently %d players in the game and you need 5-10 players." % (fname, len(game.playerlist))) 175 | 176 | 177 | def command_startgame(bot, update): 178 | log.info('command_startgame called') 179 | cid = update.message.chat_id 180 | game = GamesController.games.get(cid, None) 181 | if not game: 182 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 183 | elif game.board: 184 | bot.send_message(cid, "The game is already running!") 185 | elif update.message.from_user.id != game.initiator and bot.getChatMember(cid, update.message.from_user.id).status not in ("administrator", "creator"): 186 | bot.send_message(game.cid, "Only the initiator of the game or a group admin can start the game with /startgame") 187 | elif len(game.playerlist) < 5: 188 | bot.send_message(game.cid, "There are not enough players (min. 5, max. 10). Join the game with /join") 189 | else: 190 | player_number = len(game.playerlist) 191 | MainController.inform_players(bot, game, game.cid, player_number) 192 | MainController.inform_fascists(bot, game, player_number) 193 | game.board = Board(player_number, game) 194 | log.info(game.board) 195 | log.info("len(games) Command_startgame: " + str(len(GamesController.games))) 196 | game.shuffle_player_sequence() 197 | game.board.state.player_counter = 0 198 | bot.send_message(game.cid, game.board.print_board()) 199 | #group_name = update.message.chat.title 200 | #bot.send_message(ADMIN, "Game of Secret Blue started in group %s (%d)" % (group_name, cid)) 201 | MainController.start_round(bot, game) 202 | 203 | def command_cancelgame(bot, update): 204 | log.info('command_cancelgame called') 205 | cid = update.message.chat_id 206 | if cid in GamesController.games.keys(): 207 | game = GamesController.games[cid] 208 | status = bot.getChatMember(cid, update.message.from_user.id).status 209 | if update.message.from_user.id == game.initiator or status in ("administrator", "creator"): 210 | MainController.end_game(bot, game, 99) 211 | else: 212 | bot.send_message(cid, "Only the initiator of the game or a group admin can cancel the game with /cancelgame") 213 | else: 214 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 215 | 216 | 217 | def command_votes(bot, update): 218 | try: 219 | #Send message of executing command 220 | cid = update.message.chat_id 221 | #bot.send_message(cid, "Looking for history...") 222 | #Check if there is a current game 223 | if cid in GamesController.games.keys(): 224 | game = GamesController.games.get(cid, None) 225 | if not game.dateinitvote: 226 | # If date of init vote is null, then the voting didnt start 227 | bot.send_message(cid, "The voting didn't start yet.") 228 | else: 229 | #If there is a time, compare it and send history of votes. 230 | start = game.dateinitvote 231 | stop = datetime.datetime.now() 232 | elapsed = stop - start 233 | if elapsed > datetime.timedelta(minutes=1): 234 | history_text = "Vote history for President %s and Chancellor %s:\n\n" % (game.board.state.nominated_president.name, game.board.state.nominated_chancellor.name) 235 | for player in game.player_sequence: 236 | # If the player is in the last_votes (He voted), mark him as he registered a vote 237 | if player.uid in game.board.state.last_votes: 238 | history_text += "%s registered a vote.\n" % (game.playerlist[player.uid].name) 239 | else: 240 | history_text += "%s didn't register a vote.\n" % (game.playerlist[player.uid].name) 241 | bot.send_message(cid, history_text) 242 | else: 243 | bot.send_message(cid, "Five minutes must pass to see the votes") 244 | else: 245 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 246 | except Exception as e: 247 | bot.send_message(cid, str(e)) 248 | 249 | 250 | def command_calltovote(bot, update): 251 | try: 252 | #Send message of executing command 253 | cid = update.message.chat_id 254 | #bot.send_message(cid, "Looking for history...") 255 | #Check if there is a current game 256 | if cid in GamesController.games.keys(): 257 | game = GamesController.games.get(cid, None) 258 | if not game.dateinitvote: 259 | # If date of init vote is null, then the voting didnt start 260 | bot.send_message(cid, "The voting didn't start yet.") 261 | else: 262 | #If there is a time, compare it and send history of votes. 263 | start = game.dateinitvote 264 | stop = datetime.datetime.now() 265 | elapsed = stop - start 266 | if elapsed > datetime.timedelta(minutes=1): 267 | # Only remember to vote to players that are still in the game 268 | history_text = "" 269 | for player in game.player_sequence: 270 | # If the player is not in last_votes send him reminder 271 | if player.uid not in game.board.state.last_votes: 272 | history_text += "It's time to vote [%s](tg://user?id=%d).\n" % (game.playerlist[player.uid].name, player.uid) 273 | bot.send_message(cid, text=history_text, parse_mode=ParseMode.MARKDOWN) 274 | else: 275 | bot.send_message(cid, "Five minutes must pass to see call to vote") 276 | else: 277 | bot.send_message(cid, "There is no game in this chat. Create a new game with /newgame") 278 | except Exception as e: 279 | bot.send_message(cid, str(e)) 280 | -------------------------------------------------------------------------------- /MainController.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Julian Schrittwieser" 4 | 5 | import json 6 | import logging as log 7 | import random 8 | import re 9 | from random import randrange 10 | from time import sleep 11 | 12 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup 13 | from telegram.ext import (Updater, CommandHandler, CallbackQueryHandler) 14 | 15 | import Commands 16 | from Constants.Cards import playerSets 17 | from Constants.Config import TOKEN, STATS 18 | from Boardgamebox.Game import Game 19 | from Boardgamebox.Player import Player 20 | import GamesController 21 | 22 | import datetime 23 | 24 | # Enable logging 25 | log.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 26 | level=log.INFO, 27 | filename='../logs/logging.log') 28 | 29 | logger = log.getLogger(__name__) 30 | 31 | 32 | def initialize_testdata(): 33 | # Sample game for quicker tests 34 | testgame = Game(-1001113216265, 15771023) 35 | GamesController.games[-1001113216265] = testgame 36 | players = [Player("Александр", 320853702), Player("Gustav", 305333239), Player("Rene", 318940765), Player("Susi", 290308460), Player("Renate", 312027975)] 37 | for player in players: 38 | testgame.add_player(player.uid, player) 39 | 40 | 41 | ## 42 | # 43 | # Beginning of round 44 | # 45 | ## 46 | def start_round(bot, game): 47 | log.info('start_round called') 48 | if game.board.state.chosen_president is None: 49 | game.board.state.nominated_president = game.player_sequence[game.board.state.player_counter] 50 | else: 51 | game.board.state.nominated_president = game.board.state.chosen_president 52 | game.board.state.chosen_president = None 53 | bot.send_message(game.cid, 54 | "The next presidential canditate is %s.\n%s, please nominate a Chancellor in our private chat!" % ( 55 | game.board.state.nominated_president.name, game.board.state.nominated_president.name)) 56 | choose_chancellor(bot, game) 57 | # --> nominate_chosen_chancellor --> vote --> handle_voting --> count_votes --> voting_aftermath --> draw_policies 58 | # --> choose_policy --> pass_two_policies --> choose_policy --> enact_policy --> start_round 59 | 60 | 61 | def choose_chancellor(bot, game): 62 | log.info('choose_chancellor called') 63 | strcid = str(game.cid) 64 | pres_uid = 0 65 | chan_uid = 0 66 | btns = [] 67 | if game.board.state.president is not None: 68 | pres_uid = game.board.state.president.uid 69 | if game.board.state.chancellor is not None: 70 | chan_uid = game.board.state.chancellor.uid 71 | for uid in game.playerlist: 72 | # If there are only five players left in the 73 | # game, only the last elected Chancellor is 74 | # ineligible to be Chancellor Candidate; the 75 | # last President may be nominated. 76 | if len(game.player_sequence) > 5: 77 | if uid != game.board.state.nominated_president.uid and game.playerlist[ 78 | uid].is_dead == False and uid != pres_uid and uid != chan_uid: 79 | name = game.playerlist[uid].name 80 | btns.append([InlineKeyboardButton(name, callback_data=strcid + "_chan_" + str(uid))]) 81 | else: 82 | if uid != game.board.state.nominated_president.uid and game.playerlist[ 83 | uid].is_dead == False and uid != chan_uid: 84 | name = game.playerlist[uid].name 85 | btns.append([InlineKeyboardButton(name, callback_data=strcid + "_chan_" + str(uid))]) 86 | 87 | chancellorMarkup = InlineKeyboardMarkup(btns) 88 | bot.send_message(game.board.state.nominated_president.uid, game.board.print_board()) 89 | bot.send_message(game.board.state.nominated_president.uid, 'Please nominate your chancellor!', 90 | reply_markup=chancellorMarkup) 91 | 92 | 93 | def nominate_chosen_chancellor(bot, update): 94 | log.info('nominate_chosen_chancellor called') 95 | log.info(GamesController.games.keys()) 96 | callback = update.callback_query 97 | regex = re.search("(-[0-9]*)_chan_([0-9]*)", callback.data) 98 | cid = int(regex.group(1)) 99 | chosen_uid = int(regex.group(2)) 100 | try: 101 | game = GamesController.games.get(cid, None) 102 | log.info(game) 103 | log.info(game.board) 104 | game.board.state.nominated_chancellor = game.playerlist[chosen_uid] 105 | log.info("President %s (%d) nominated %s (%d)" % ( 106 | game.board.state.nominated_president.name, game.board.state.nominated_president.uid, 107 | game.board.state.nominated_chancellor.name, game.board.state.nominated_chancellor.uid)) 108 | bot.edit_message_text("You nominated %s as Chancellor!" % game.board.state.nominated_chancellor.name, 109 | callback.from_user.id, callback.message.message_id) 110 | bot.send_message(game.cid, 111 | "President %s nominated %s as Chancellor. Please vote now!" % ( 112 | game.board.state.nominated_president.name, game.board.state.nominated_chancellor.name)) 113 | vote(bot, game) 114 | except AttributeError as e: 115 | log.error("nominate_chosen_chancellor: Game or board should not be None! Eror: " + str(e)) 116 | except Exception as e: 117 | log.error("Unknown error: " + str(e)) 118 | 119 | 120 | def vote(bot, game): 121 | log.info('vote called') 122 | #When voting starts we start the counter to see later with the vote/calltovote command we can see who voted. 123 | game.dateinitvote = datetime.datetime.now() 124 | strcid = str(game.cid) 125 | btns = [[InlineKeyboardButton("Ja", callback_data=strcid + "_Ja"), 126 | InlineKeyboardButton("Nein", callback_data=strcid + "_Nein")]] 127 | voteMarkup = InlineKeyboardMarkup(btns) 128 | for uid in game.playerlist: 129 | if not game.playerlist[uid].is_dead: 130 | if game.playerlist[uid] is not game.board.state.nominated_president: 131 | # the nominated president already got the board before nominating a chancellor 132 | bot.send_message(uid, game.board.print_board()) 133 | bot.send_message(uid, 134 | "Do you want to elect President %s and Chancellor %s?" % ( 135 | game.board.state.nominated_president.name, game.board.state.nominated_chancellor.name), 136 | reply_markup=voteMarkup) 137 | 138 | 139 | def handle_voting(bot, update): 140 | callback = update.callback_query 141 | log.info('handle_voting called: %s' % callback.data) 142 | regex = re.search("(-[0-9]*)_(.*)", callback.data) 143 | cid = int(regex.group(1)) 144 | answer = regex.group(2) 145 | try: 146 | game = GamesController.games[cid] 147 | uid = callback.from_user.id 148 | bot.edit_message_text("Thank you for your vote: %s to a President %s and a Chancellor %s" % ( 149 | answer, game.board.state.nominated_president.name, game.board.state.nominated_chancellor.name), uid, 150 | callback.message.message_id) 151 | log.info("Player %s (%d) voted %s" % (callback.from_user.first_name, uid, answer)) 152 | if uid not in game.board.state.last_votes: 153 | game.board.state.last_votes[uid] = answer 154 | if len(game.board.state.last_votes) == len(game.player_sequence): 155 | count_votes(bot, game) 156 | except: 157 | log.error("handle_voting: Game or board should not be None!") 158 | 159 | 160 | def count_votes(bot, game): 161 | log.info('count_votes called') 162 | # Voted Ended 163 | game.dateinitvote = None 164 | voting_text = "" 165 | voting_success = False 166 | for player in game.player_sequence: 167 | if game.board.state.last_votes[player.uid] == "Ja": 168 | voting_text += game.playerlist[player.uid].name + " voted Ja!\n" 169 | elif game.board.state.last_votes[player.uid] == "Nein": 170 | voting_text += game.playerlist[player.uid].name + " voted Nein!\n" 171 | if list(game.board.state.last_votes.values()).count("Ja") > ( 172 | len(game.player_sequence) / 2): # because player_sequence doesnt include dead 173 | # VOTING WAS SUCCESSFUL 174 | log.info("Voting successful") 175 | voting_text += "Hail President %s! Hail Chancellor %s!" % ( 176 | game.board.state.nominated_president.name, game.board.state.nominated_chancellor.name) 177 | game.board.state.chancellor = game.board.state.nominated_chancellor 178 | game.board.state.president = game.board.state.nominated_president 179 | game.board.state.nominated_president = None 180 | game.board.state.nominated_chancellor = None 181 | voting_success = True 182 | bot.send_message(game.cid, voting_text) 183 | voting_aftermath(bot, game, voting_success) 184 | else: 185 | log.info("Voting failed") 186 | voting_text += "The people didn't like the two candidates!" 187 | game.board.state.nominated_president = None 188 | game.board.state.nominated_chancellor = None 189 | game.board.state.failed_votes += 1 190 | bot.send_message(game.cid, voting_text) 191 | if game.board.state.failed_votes == 3: 192 | do_anarchy(bot, game) 193 | else: 194 | voting_aftermath(bot, game, voting_success) 195 | 196 | 197 | def voting_aftermath(bot, game, voting_success): 198 | log.info('voting_aftermath called') 199 | game.board.state.last_votes = {} 200 | if voting_success: 201 | if game.board.state.fascist_track >= 3 and game.board.state.chancellor.role == "Blue": 202 | # fascists win, because Blue was elected as chancellor after 3 fascist policies 203 | game.board.state.game_endcode = -2 204 | end_game(bot, game, game.board.state.game_endcode) 205 | elif game.board.state.fascist_track >= 3 and game.board.state.chancellor.role != "Blue" and game.board.state.chancellor not in game.board.state.not_blues: 206 | game.board.state.not_blues.append(game.board.state.chancellor) 207 | draw_policies(bot, game) 208 | else: 209 | # voting was successful and Blue was not nominated as chancellor after 3 fascist policies 210 | draw_policies(bot, game) 211 | else: 212 | bot.send_message(game.cid, game.board.print_board()) 213 | start_next_round(bot, game) 214 | 215 | 216 | def draw_policies(bot, game): 217 | log.info('draw_policies called') 218 | strcid = str(game.cid) 219 | game.board.state.veto_refused = False 220 | # shuffle discard pile with rest if rest < 3 221 | shuffle_policy_pile(bot, game) 222 | btns = [] 223 | for i in range(3): 224 | game.board.state.drawn_policies.append(game.board.policies.pop(0)) 225 | for policy in game.board.state.drawn_policies: 226 | btns.append([InlineKeyboardButton(policy, callback_data=strcid + "_" + policy)]) 227 | 228 | choosePolicyMarkup = InlineKeyboardMarkup(btns) 229 | bot.send_message(game.board.state.president.uid, 230 | "You drew the following 3 policies. Which one do you want to discard?", 231 | reply_markup=choosePolicyMarkup) 232 | 233 | 234 | def choose_policy(bot, update): 235 | log.info('choose_policy called') 236 | callback = update.callback_query 237 | regex = re.search("(-[0-9]*)_(.*)", callback.data) 238 | cid = int(regex.group(1)) 239 | answer = regex.group(2) 240 | try: 241 | game = GamesController.games[cid] 242 | strcid = str(game.cid) 243 | uid = callback.from_user.id 244 | if len(game.board.state.drawn_policies) == 3: 245 | log.info("Player %s (%d) discarded %s" % (callback.from_user.first_name, uid, answer)) 246 | bot.edit_message_text("The policy %s will be discarded!" % answer, uid, 247 | callback.message.message_id) 248 | # remove policy from drawn cards and add to discard pile, pass the other two policies 249 | for i in range(3): 250 | if game.board.state.drawn_policies[i] == answer: 251 | game.board.discards.append(game.board.state.drawn_policies.pop(i)) 252 | break 253 | pass_two_policies(bot, game) 254 | elif len(game.board.state.drawn_policies) == 2: 255 | if answer == "veto": 256 | log.info("Player %s (%d) suggested a veto" % (callback.from_user.first_name, uid)) 257 | bot.edit_message_text("You suggested a Veto to President %s" % game.board.state.president.name, uid, 258 | callback.message.message_id) 259 | bot.send_message(game.cid, 260 | "Chancellor %s suggested a Veto to President %s." % ( 261 | game.board.state.chancellor.name, game.board.state.president.name)) 262 | 263 | btns = [[InlineKeyboardButton("Veto! (accept suggestion)", callback_data=strcid + "_yesveto")], 264 | [InlineKeyboardButton("No Veto! (refuse suggestion)", callback_data=strcid + "_noveto")]] 265 | 266 | vetoMarkup = InlineKeyboardMarkup(btns) 267 | bot.send_message(game.board.state.president.uid, 268 | "Chancellor %s suggested a Veto to you. Do you want to veto (discard) these cards?" % game.board.state.chancellor.name, 269 | reply_markup=vetoMarkup) 270 | else: 271 | log.info("Player %s (%d) chose a %s policy" % (callback.from_user.first_name, uid, answer)) 272 | bot.edit_message_text("The policy %s will be enacted!" % answer, uid, 273 | callback.message.message_id) 274 | # remove policy from drawn cards and enact, discard the other card 275 | for i in range(2): 276 | if game.board.state.drawn_policies[i] == answer: 277 | game.board.state.drawn_policies.pop(i) 278 | break 279 | game.board.discards.append(game.board.state.drawn_policies.pop(0)) 280 | assert len(game.board.state.drawn_policies) == 0 281 | enact_policy(bot, game, answer, False) 282 | else: 283 | log.error("choose_policy: drawn_policies should be 3 or 2, but was " + str( 284 | len(game.board.state.drawn_policies))) 285 | except: 286 | log.error("choose_policy: Game or board should not be None!") 287 | 288 | 289 | def pass_two_policies(bot, game): 290 | log.info('pass_two_policies called') 291 | strcid = str(game.cid) 292 | btns = [] 293 | for policy in game.board.state.drawn_policies: 294 | btns.append([InlineKeyboardButton(policy, callback_data=strcid + "_" + policy)]) 295 | if game.board.state.fascist_track == 5 and not game.board.state.veto_refused: 296 | btns.append([InlineKeyboardButton("Veto", callback_data=strcid + "_veto")]) 297 | choosePolicyMarkup = InlineKeyboardMarkup(btns) 298 | bot.send_message(game.cid, 299 | "President %s gave two policies to Chancellor %s." % ( 300 | game.board.state.president.name, game.board.state.chancellor.name)) 301 | bot.send_message(game.board.state.chancellor.uid, 302 | "President %s gave you the following 2 policies. Which one do you want to enact? You can also use your Veto power." % game.board.state.president.name, 303 | reply_markup=choosePolicyMarkup) 304 | elif game.board.state.veto_refused: 305 | choosePolicyMarkup = InlineKeyboardMarkup(btns) 306 | bot.send_message(game.board.state.chancellor.uid, 307 | "President %s refused your Veto. Now you have to choose. Which one do you want to enact?" % game.board.state.president.name, 308 | reply_markup=choosePolicyMarkup) 309 | elif game.board.state.fascist_track < 5: 310 | choosePolicyMarkup = InlineKeyboardMarkup(btns) 311 | bot.send_message(game.board.state.chancellor.uid, 312 | "President %s gave you the following 2 policies. Which one do you want to enact?" % game.board.state.president.name, 313 | reply_markup=choosePolicyMarkup) 314 | 315 | 316 | def enact_policy(bot, game, policy, anarchy): 317 | log.info('enact_policy called') 318 | if policy == "liberal": 319 | game.board.state.liberal_track += 1 320 | elif policy == "fascist": 321 | game.board.state.fascist_track += 1 322 | game.board.state.failed_votes = 0 # reset counter 323 | if not anarchy: 324 | bot.send_message(game.cid, 325 | "President %s and Chancellor %s enacted a %s policy!" % ( 326 | game.board.state.president.name, game.board.state.chancellor.name, policy)) 327 | else: 328 | bot.send_message(game.cid, 329 | "The top most policy was enacted: %s" % policy) 330 | sleep(3) 331 | bot.send_message(game.cid, game.board.print_board()) 332 | # end of round 333 | if game.board.state.liberal_track == 5: 334 | game.board.state.game_endcode = 1 335 | end_game(bot, game, game.board.state.game_endcode) # liberals win with 5 liberal policies 336 | if game.board.state.fascist_track == 6: 337 | game.board.state.game_endcode = -1 338 | end_game(bot, game, game.board.state.game_endcode) # fascists win with 6 fascist policies 339 | sleep(3) 340 | # End of legislative session, shuffle if necessary 341 | shuffle_policy_pile(bot, game) 342 | if not anarchy: 343 | if policy == "fascist": 344 | action = game.board.fascist_track_actions[game.board.state.fascist_track - 1] 345 | if action is None and game.board.state.fascist_track == 6: 346 | pass 347 | elif action == None: 348 | start_next_round(bot, game) 349 | elif action == "policy": 350 | bot.send_message(game.cid, 351 | "Presidential Power enabled: Policy Peek " + u"\U0001F52E" + "\nPresident %s now knows the next three policies on " 352 | "the pile. The President may share " 353 | "(or lie about!) the results of their " 354 | "investigation at their discretion." % game.board.state.president.name) 355 | action_policy(bot, game) 356 | elif action == "kill": 357 | bot.send_message(game.cid, 358 | "Presidential Power enabled: Execution " + u"\U0001F5E1" + "\nPresident %s has to kill one person. You can " 359 | "discuss the decision now but the " 360 | "President has the final say." % game.board.state.president.name) 361 | action_kill(bot, game) 362 | elif action == "inspect": 363 | bot.send_message(game.cid, 364 | "Presidential Power enabled: Investigate Loyalty " + u"\U0001F50E" + "\nPresident %s may see the party membership of one " 365 | "player. The President may share " 366 | "(or lie about!) the results of their " 367 | "investigation at their discretion." % game.board.state.president.name) 368 | action_inspect(bot, game) 369 | elif action == "choose": 370 | bot.send_message(game.cid, 371 | "Presidential Power enabled: Call Special Election " + u"\U0001F454" + "\nPresident %s gets to choose the next presidential " 372 | "candidate. Afterwards the order resumes " 373 | "back to normal." % game.board.state.president.name) 374 | action_choose(bot, game) 375 | else: 376 | start_next_round(bot, game) 377 | else: 378 | start_next_round(bot, game) 379 | 380 | 381 | def choose_veto(bot, update): 382 | log.info('choose_veto called') 383 | callback = update.callback_query 384 | regex = re.search("(-[0-9]*)_(.*)", callback.data) 385 | cid = int(regex.group(1)) 386 | answer = regex.group(2) 387 | try: 388 | game = GamesController.games[cid] 389 | uid = callback.from_user.id 390 | if answer == "yesveto": 391 | log.info("Player %s (%d) accepted the veto" % (callback.from_user.first_name, uid)) 392 | bot.edit_message_text("You accepted the Veto!", uid, callback.message.message_id) 393 | bot.send_message(game.cid, 394 | "President %s accepted Chancellor %s's Veto. No policy was enacted but this counts as a failed election." % ( 395 | game.board.state.president.name, game.board.state.chancellor.name)) 396 | game.board.discards += game.board.state.drawn_policies 397 | game.board.state.drawn_policies = [] 398 | game.board.state.failed_votes += 1 399 | if game.board.state.failed_votes == 3: 400 | do_anarchy(bot, game) 401 | else: 402 | bot.send_message(game.cid, game.board.print_board()) 403 | start_next_round(bot, game) 404 | elif answer == "noveto": 405 | log.info("Player %s (%d) declined the veto" % (callback.from_user.first_name, uid)) 406 | game.board.state.veto_refused = True 407 | bot.edit_message_text("You refused the Veto!", uid, callback.message.message_id) 408 | bot.send_message(game.cid, 409 | "President %s refused Chancellor %s's Veto. The Chancellor now has to choose a policy!" % ( 410 | game.board.state.president.name, game.board.state.chancellor.name)) 411 | pass_two_policies(bot, game) 412 | else: 413 | log.error("choose_veto: Callback data can either be \"veto\" or \"noveto\", but not %s" % answer) 414 | except: 415 | log.error("choose_veto: Game or board should not be None!") 416 | 417 | 418 | def do_anarchy(bot, game): 419 | log.info('do_anarchy called') 420 | bot.send_message(game.cid, game.board.print_board()) 421 | bot.send_message(game.cid, "ANARCHY!!") 422 | game.board.state.president = None 423 | game.board.state.chancellor = None 424 | top_policy = game.board.policies.pop(0) 425 | game.board.state.last_votes = {} 426 | enact_policy(bot, game, top_policy, True) 427 | 428 | 429 | def action_policy(bot, game): 430 | log.info('action_policy called') 431 | topPolicies = "" 432 | # shuffle discard pile with rest if rest < 3 433 | shuffle_policy_pile(bot, game) 434 | for i in range(3): 435 | topPolicies += game.board.policies[i] + "\n" 436 | bot.send_message(game.board.state.president.uid, 437 | "The top three polices are (top most first):\n%s\nYou may lie about this." % topPolicies) 438 | start_next_round(bot, game) 439 | 440 | 441 | def action_kill(bot, game): 442 | log.info('action_kill called') 443 | strcid = str(game.cid) 444 | btns = [] 445 | for uid in game.playerlist: 446 | if uid != game.board.state.president.uid and game.playerlist[uid].is_dead == False: 447 | name = game.playerlist[uid].name 448 | btns.append([InlineKeyboardButton(name, callback_data=strcid + "_kill_" + str(uid))]) 449 | 450 | killMarkup = InlineKeyboardMarkup(btns) 451 | bot.send_message(game.board.state.president.uid, game.board.print_board()) 452 | bot.send_message(game.board.state.president.uid, 453 | 'You have to kill one person. You can discuss your decision with the others. Choose wisely!', 454 | reply_markup=killMarkup) 455 | 456 | 457 | def choose_kill(bot, update): 458 | log.info('choose_kill called') 459 | callback = update.callback_query 460 | regex = re.search("(-[0-9]*)_kill_(.*)", callback.data) 461 | cid = int(regex.group(1)) 462 | answer = int(regex.group(2)) 463 | try: 464 | game = GamesController.games[cid] 465 | chosen = game.playerlist[answer] 466 | chosen.is_dead = True 467 | if game.player_sequence.index(chosen) <= game.board.state.player_counter: 468 | game.board.state.player_counter -= 1 469 | game.player_sequence.remove(chosen) 470 | game.board.state.dead += 1 471 | log.info("Player %s (%d) killed %s (%d)" % ( 472 | callback.from_user.first_name, callback.from_user.id, chosen.name, chosen.uid)) 473 | bot.edit_message_text("You killed %s!" % chosen.name, callback.from_user.id, callback.message.message_id) 474 | if chosen.role == "Blue": 475 | bot.send_message(game.cid, "President " + game.board.state.president.name + " killed " + chosen.name + ". ") 476 | end_game(bot, game, 2) 477 | else: 478 | bot.send_message(game.cid, 479 | "President %s killed %s who was not Blue. %s, you are dead now and are not allowed to talk anymore!" % ( 480 | game.board.state.president.name, chosen.name, chosen.name)) 481 | bot.send_message(game.cid, game.board.print_board()) 482 | start_next_round(bot, game) 483 | except: 484 | log.error("choose_kill: Game or board should not be None!") 485 | 486 | 487 | def action_choose(bot, game): 488 | log.info('action_choose called') 489 | strcid = str(game.cid) 490 | btns = [] 491 | 492 | for uid in game.playerlist: 493 | if uid != game.board.state.president.uid and game.playerlist[uid].is_dead == False: 494 | name = game.playerlist[uid].name 495 | btns.append([InlineKeyboardButton(name, callback_data=strcid + "_choo_" + str(uid))]) 496 | 497 | inspectMarkup = InlineKeyboardMarkup(btns) 498 | bot.send_message(game.board.state.president.uid, game.board.print_board()) 499 | bot.send_message(game.board.state.president.uid, 500 | 'You get to choose the next presidential candidate. Afterwards the order resumes back to normal. Choose wisely!', 501 | reply_markup=inspectMarkup) 502 | 503 | 504 | def choose_choose(bot, update): 505 | log.info('choose_choose called') 506 | callback = update.callback_query 507 | regex = re.search("(-[0-9]*)_choo_(.*)", callback.data) 508 | cid = int(regex.group(1)) 509 | answer = int(regex.group(2)) 510 | try: 511 | game = GamesController.games[cid] 512 | chosen = game.playerlist[answer] 513 | game.board.state.chosen_president = chosen 514 | log.info( 515 | "Player %s (%d) chose %s (%d) as next president" % ( 516 | callback.from_user.first_name, callback.from_user.id, chosen.name, chosen.uid)) 517 | bot.edit_message_text("You chose %s as the next president!" % chosen.name, callback.from_user.id, 518 | callback.message.message_id) 519 | bot.send_message(game.cid, 520 | "President %s chose %s as the next president." % ( 521 | game.board.state.president.name, chosen.name)) 522 | start_next_round(bot, game) 523 | except: 524 | log.error("choose_choose: Game or board should not be None!") 525 | 526 | 527 | def action_inspect(bot, game): 528 | log.info('action_inspect called') 529 | strcid = str(game.cid) 530 | btns = [] 531 | for uid in game.playerlist: 532 | if uid != game.board.state.president.uid and game.playerlist[uid].is_dead == False: 533 | name = game.playerlist[uid].name 534 | btns.append([InlineKeyboardButton(name, callback_data=strcid + "_insp_" + str(uid))]) 535 | 536 | inspectMarkup = InlineKeyboardMarkup(btns) 537 | bot.send_message(game.board.state.president.uid, game.board.print_board()) 538 | bot.send_message(game.board.state.president.uid, 539 | 'You may see the party membership of one player. Which do you want to know? Choose wisely!', 540 | reply_markup=inspectMarkup) 541 | 542 | 543 | def choose_inspect(bot, update): 544 | log.info('choose_inspect called') 545 | callback = update.callback_query 546 | regex = re.search("(-[0-9]*)_insp_(.*)", callback.data) 547 | cid = int(regex.group(1)) 548 | answer = int(regex.group(2)) 549 | try: 550 | game = GamesController.games[cid] 551 | chosen = game.playerlist[answer] 552 | log.info( 553 | "Player %s (%d) inspects %s (%d)'s party membership (%s)" % ( 554 | callback.from_user.first_name, callback.from_user.id, chosen.name, chosen.uid, 555 | chosen.party)) 556 | bot.edit_message_text("The party membership of %s is %s" % (chosen.name, chosen.party), 557 | callback.from_user.id, 558 | callback.message.message_id) 559 | bot.send_message(game.cid, "President %s inspected %s." % (game.board.state.president.name, chosen.name)) 560 | start_next_round(bot, game) 561 | except: 562 | log.error("choose_inspect: Game or board should not be None!") 563 | 564 | 565 | def start_next_round(bot, game): 566 | log.info('start_next_round called') 567 | # start next round if there is no winner (or /cancel) 568 | if game.board.state.game_endcode == 0: 569 | # start new round 570 | sleep(5) 571 | # if there is no special elected president in between 572 | if game.board.state.chosen_president is None: 573 | increment_player_counter(game) 574 | start_round(bot, game) 575 | 576 | 577 | ## 578 | # 579 | # End of round 580 | # 581 | ## 582 | 583 | def end_game(bot, game, game_endcode): 584 | log.info('end_game called') 585 | ## 586 | # game_endcode: 587 | # -2 fascists win by electing Blue as chancellor 588 | # -1 fascists win with 6 fascist policies 589 | # 0 not ended 590 | # 1 liberals win with 5 liberal policies 591 | # 2 liberals win by killing Blue 592 | # 99 game cancelled 593 | # 594 | with open(STATS, 'r') as f: 595 | stats = json.load(f) 596 | 597 | if game_endcode == 99: 598 | if GamesController.games[game.cid].board is not None: 599 | bot.send_message(game.cid, 600 | "Game cancelled!\n\n%s" % game.print_roles()) 601 | # bot.send_message(ADMIN, "Game of Secret Blue canceled in group %d" % game.cid) 602 | stats['cancelled'] = stats['cancelled'] + 1 603 | else: 604 | bot.send_message(game.cid, "Game cancelled!") 605 | else: 606 | if game_endcode == -2: 607 | bot.send_message(game.cid, 608 | "Game over! The fascists win by electing Blue as Chancellor!\n\n%s" % game.print_roles()) 609 | stats['fascwin_blue'] = stats['fascwin_blue'] + 1 610 | if game_endcode == -1: 611 | bot.send_message(game.cid, 612 | "Game over! The fascists win by enacting 6 fascist policies!\n\n%s" % game.print_roles()) 613 | stats['fascwin_policies'] = stats['fascwin_policies'] + 1 614 | if game_endcode == 1: 615 | bot.send_message(game.cid, 616 | "Game over! The liberals win by enacting 5 liberal policies!\n\n%s" % game.print_roles()) 617 | stats['libwin_policies'] = stats['libwin_policies'] + 1 618 | if game_endcode == 2: 619 | bot.send_message(game.cid, 620 | "Game over! The liberals win by killing Blue!\n\n%s" % game.print_roles()) 621 | stats['libwin_kill'] = stats['libwin_kill'] + 1 622 | 623 | # bot.send_message(ADMIN, "Game of Secret Blue ended in group %d" % game.cid) 624 | 625 | with open(STATS, 'w') as f: 626 | json.dump(stats, f) 627 | del GamesController.games[game.cid] 628 | 629 | 630 | def inform_players(bot, game, cid, player_number): 631 | log.info('inform_players called') 632 | bot.send_message(cid, 633 | "Let's start the game with %d players!\n%s\nGo to your private chat and look at your secret role!" % ( 634 | player_number, print_player_info(player_number))) 635 | available_roles = list(playerSets[player_number]["roles"]) # copy not reference because we need it again later 636 | for uid in game.playerlist: 637 | random_index = randrange(len(available_roles)) 638 | role = available_roles.pop(random_index) 639 | party = get_membership(role) 640 | game.playerlist[uid].role = role 641 | game.playerlist[uid].party = party 642 | bot.send_message(uid, "Your secret role is: %s\nYour party membership is: %s" % (role, party)) 643 | 644 | 645 | def print_player_info(player_number): 646 | if player_number == 5: 647 | return "There are 3 Liberals, 1 Fascist and Blue. Blue knows who the Fascist is." 648 | elif player_number == 6: 649 | return "There are 4 Liberals, 1 Fascist and Blue. Blue knows who the Fascist is." 650 | elif player_number == 7: 651 | return "There are 4 Liberals, 2 Fascist and Blue. Blue doesn't know who the Fascists are." 652 | elif player_number == 8: 653 | return "There are 5 Liberals, 2 Fascist and Blue. Blue doesn't know who the Fascists are." 654 | elif player_number == 9: 655 | return "There are 5 Liberals, 3 Fascist and Blue. Blue doesn't know who the Fascists are." 656 | elif player_number == 10: 657 | return "There are 6 Liberals, 3 Fascist and Blue. Blue doesn't know who the Fascists are." 658 | 659 | 660 | def inform_fascists(bot, game, player_number): 661 | log.info('inform_fascists called') 662 | 663 | for uid in game.playerlist: 664 | role = game.playerlist[uid].role 665 | if role == "Fascist": 666 | fascists = game.get_fascists() 667 | if player_number > 6: 668 | fstring = "" 669 | for f in fascists: 670 | if f.uid != uid: 671 | fstring += f.name + ", " 672 | fstring = fstring[:-2] 673 | bot.send_message(uid, "Your fellow fascists are: %s" % fstring) 674 | blue = game.get_blue() 675 | bot.send_message(uid, "Blue is: %s" % blue.name) 676 | elif role == "Blue": 677 | if player_number <= 6: 678 | fascists = game.get_fascists() 679 | bot.send_message(uid, "Your fellow fascist is: %s" % fascists[0].name) 680 | elif role == "Liberal": 681 | pass 682 | else: 683 | log.error("inform_fascists: can\'t handle the role %s" % role) 684 | 685 | 686 | def get_membership(role): 687 | log.info('get_membership called') 688 | if role == "Fascist" or role == "Blue": 689 | return "fascist" 690 | elif role == "Liberal": 691 | return "liberal" 692 | else: 693 | return None 694 | 695 | 696 | def increment_player_counter(game): 697 | log.info('increment_player_counter called') 698 | if game.board.state.player_counter < len(game.player_sequence) - 1: 699 | game.board.state.player_counter += 1 700 | else: 701 | game.board.state.player_counter = 0 702 | 703 | 704 | def shuffle_policy_pile(bot, game): 705 | log.info('shuffle_policy_pile called') 706 | if len(game.board.policies) < 3: 707 | game.board.discards += game.board.policies 708 | game.board.policies = random.sample(game.board.discards, len(game.board.discards)) 709 | game.board.discards = [] 710 | bot.send_message(game.cid, 711 | "There were not enough cards left on the policy pile so I shuffled the rest with the discard pile!") 712 | 713 | 714 | def error(bot, update, error): 715 | logger.warning('Update "%s" caused error "%s"' % (update, error)) 716 | 717 | 718 | def main(): 719 | GamesController.init() #Call only once 720 | #initialize_testdata() 721 | 722 | updater = Updater(TOKEN) 723 | 724 | # Get the dispatcher to register handlers 725 | dp = updater.dispatcher 726 | 727 | # on different commands - answer in Telegram 728 | dp.add_handler(CommandHandler("start", Commands.command_start)) 729 | dp.add_handler(CommandHandler("help", Commands.command_help)) 730 | dp.add_handler(CommandHandler("board", Commands.command_board)) 731 | dp.add_handler(CommandHandler("rules", Commands.command_rules)) 732 | dp.add_handler(CommandHandler("ping", Commands.command_ping)) 733 | dp.add_handler(CommandHandler("symbols", Commands.command_symbols)) 734 | dp.add_handler(CommandHandler("stats", Commands.command_stats)) 735 | dp.add_handler(CommandHandler("newgame", Commands.command_newgame)) 736 | dp.add_handler(CommandHandler("startgame", Commands.command_startgame)) 737 | dp.add_handler(CommandHandler("cancelgame", Commands.command_cancelgame)) 738 | dp.add_handler(CommandHandler("join", Commands.command_join)) 739 | dp.add_handler(CommandHandler("votes", Commands.command_votes)) 740 | dp.add_handler(CommandHandler("calltovote", Commands.command_calltovote)) 741 | 742 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_chan_(.*)", callback=nominate_chosen_chancellor)) 743 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_insp_(.*)", callback=choose_inspect)) 744 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_choo_(.*)", callback=choose_choose)) 745 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_kill_(.*)", callback=choose_kill)) 746 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_(yesveto|noveto)", callback=choose_veto)) 747 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_(liberal|fascist|veto)", callback=choose_policy)) 748 | dp.add_handler(CallbackQueryHandler(pattern="(-[0-9]*)_(Ja|Nein)", callback=handle_voting)) 749 | 750 | # log all errors 751 | dp.add_error_handler(error) 752 | 753 | # Start the Bot 754 | updater.start_polling() 755 | 756 | # Run the bot until the you presses Ctrl-C or the process receives SIGINT, 757 | # SIGTERM or SIGABRT. This should be used most of the time, since 758 | # start_polling() is non-blocking and will stop the bot gracefully. 759 | updater.idle() 760 | 761 | 762 | if __name__ == '__main__': 763 | main() 764 | --------------------------------------------------------------------------------