diff --git a/tools/provision b/tools/provision index e6894ea04..c0d143a86 100755 --- a/tools/provision +++ b/tools/provision @@ -116,6 +116,9 @@ the Python version this command is executed with.""" venv_dir, venv_exec_dir, 'activate') + # We make the path look like a Unix path, because most Windows users + # are likely to be running in a bash shell. + activate_command = activate_command.replace(os.sep, '/') print('\nRun the following to enter into the virtualenv:\n') print(bold + ' source ' + activate_command + end_format + "\n") diff --git a/tools/run-mypy b/tools/run-mypy index 357c599e0..e5706f3fe 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -90,6 +90,8 @@ force_include = [ "zulip_bots/zulip_bots/bots/tictactoe/test_tictactoe.py", "zulip_bots/zulip_bots/bots/trivia_quiz/trivia_quiz.py", "zulip_bots/zulip_bots/bots/trivia_quiz/test_trivia_quiz.py", + "zulip_bots/zulip_bots/bots/trivia_quiz_game/trivia_quiz_game.py", + "zulip_bots/zulip_bots/bots/trivia_quiz_game/test_trivia_quiz_game.py", "zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py", "zulip_bots/zulip_bots/bots/game_handler_bot/test_game_handler_bot.py", "zulip_bots/zulip_bots/bots/trello/trello.py", diff --git a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py index 8bd983ff1..dabb58239 100644 --- a/zulip_bots/zulip_bots/bots/connect_four/connect_four.py +++ b/zulip_bots/zulip_bots/bots/connect_four/connect_four.py @@ -25,7 +25,7 @@ def parse_board(self, board: Any) -> str: def get_player_color(self, turn: int) -> str: return self.tokens[turn] - def alert_move_message(self, original_player: str, move_info: str) -> str: + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: column_number = move_info.replace('move ', '') return original_player + ' moved in column ' + column_number diff --git a/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py b/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py index 6cd168cee..44f9f1f5d 100644 --- a/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py +++ b/zulip_bots/zulip_bots/bots/game_handler_bot/game_handler_bot.py @@ -11,7 +11,7 @@ def parse_board(self, board: Any) -> str: def get_player_color(self, turn: int) -> str: return self.tokens[turn] - def alert_move_message(self, original_player: str, move_info: str) -> str: + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: column_number = move_info.replace('move ', '') return original_player + ' moved in column ' + column_number diff --git a/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py b/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py index 3706445c0..4e09b3bed 100644 --- a/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py +++ b/zulip_bots/zulip_bots/bots/game_of_fifteen/game_of_fifteen.py @@ -107,7 +107,7 @@ def parse_board(self, board: Any) -> str: board_str += self.tiles[str(board[row][column])] return board_str - def alert_move_message(self, original_player: str, move_info: str) -> str: + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: tile = move_info.replace('move ', '') return original_player + ' moved ' + tile diff --git a/zulip_bots/zulip_bots/bots/merels/merels.py b/zulip_bots/zulip_bots/bots/merels/merels.py index 85d4e3896..5316cf055 100644 --- a/zulip_bots/zulip_bots/bots/merels/merels.py +++ b/zulip_bots/zulip_bots/bots/merels/merels.py @@ -63,7 +63,7 @@ def parse_board(self, board: Any) -> str: def get_player_color(self, turn: int) -> str: return self.tokens[turn] - def alert_move_message(self, original_player: str, move_info: str) -> str: + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: return original_player + " :" + move_info def game_start_message(self) -> str: diff --git a/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py b/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py index 840e4b17f..eb544f4ee 100644 --- a/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py +++ b/zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py @@ -238,7 +238,7 @@ def parse_board(self, board: Any) -> str: def get_player_color(self, turn: int) -> str: return self.tokens[turn] - def alert_move_message(self, original_player: str, move_info: str) -> str: + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: move_info = move_info.replace('move ', '') return '{} put a token at {}'.format(original_player, move_info) diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/__init__.py b/zulip_bots/zulip_bots/bots/trivia_quiz_game/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/controller.py b/zulip_bots/zulip_bots/bots/trivia_quiz_game/controller.py new file mode 100644 index 000000000..b75a1431d --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/controller.py @@ -0,0 +1,102 @@ +from zulip_bots.game_handler import BadMoveException +import html +import requests +import random + +from typing import Optional, Any, Dict + +class NotAvailableException(Exception): + def __init__(self, message: str) -> None: + self.message = message + + def __str__(self) -> str: + return self.message + +class TriviaQuizGameModel(object): + def __init__(self): + # This could throw an exception. It will be picked up by + # game_handler and the game will end + self.current_board = self.get_trivia_quiz() + self.scores = {} # type: Dict[int, int] + + def validate_move(self, answer): + return answer in "ABCD" + + def make_move(self, move, player_number, is_computer=False): + if player_number not in self.scores: + self.scores[player_number] = 0 + if not self.validate_move(move): + raise BadMoveException("Move not valid") + if move == self.current_board['correct_letter']: + self.scores[player_number] += 1 + else: + self.scores[player_number] -= 1 + if self.scores[player_number] < 0: + self.scores[player_number] = 0 + self.current_board = self.get_trivia_quiz() + return { + 'correct': move == self.current_board['correct_letter'], + 'correct_letter': self.current_board['correct_letter'], + 'score': self.scores[player_number] + } + + def determine_game_over(self, players): + for player_number, score in self.scores.items(): + if score >= 5: + # Game over + return players[player_number] + return + + def get_trivia_quiz(self) -> Dict[str, Any]: + payload = self.get_trivia_payload() + quiz = self.get_quiz_from_payload(payload) + return quiz + + def get_trivia_payload(self) -> Dict[str, Any]: + + url = 'https://opentdb.com/api.php?amount=1&type=multiple' + + try: + data = requests.get(url) + + except requests.exceptions.RequestException: + raise NotAvailableException("Uh-Oh! Trivia service is down.") + + if data.status_code != 200: + raise NotAvailableException("Uh-Oh! Trivia service is down.") + + payload = data.json() + return payload + + def get_quiz_from_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]: + result = payload['results'][0] + question = result['question'] + letters = ['A', 'B', 'C', 'D'] + random.shuffle(letters) + correct_letter = letters[0] + answers = dict() + answers[correct_letter] = result['correct_answer'] + for i in range(3): + answers[letters[i+1]] = result['incorrect_answers'][i] + + def fix_quotes(s: str) -> Optional[str]: + # opentdb is nice enough to escape HTML for us, but + # we are sending this to code that does that already :) + # + # Meanwhile Python took until version 3.4 to have a + # simple html.unescape function. + try: + return html.unescape(s) + except Exception: + raise Exception('Please use python3.4 or later for this bot.') + answers = { + letter: fix_quotes(answer) + for letter, answer + in answers.items() + } + quiz = dict( + question=fix_quotes(question), + answers=answers, + correct_letter=correct_letter, + ) + return quiz diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question.json b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question.json new file mode 100644 index 000000000..c5678b5f0 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question.json @@ -0,0 +1,23 @@ +{ + "request":{ + "api_url":"https://opentdb.com/api.php?amount=1&type=multiple" + }, + "response":{ + "response_code":0, + "results":[ + { + "category":"Animals", + "type":"multiple", + "difficulty":"easy", + "question":"Which class of animals are newts members of?", + "correct_answer":"Amphibian", + "incorrect_answers":["Fish","Reptiles","Mammals"] + } + ] + }, + "response-headers":{ + "status":200, + "ok":true, + "content-type":"application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question_dict.json b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question_dict.json new file mode 100644 index 000000000..a7419c093 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_new_question_dict.json @@ -0,0 +1,10 @@ +{ + "question": "Which class of animals are newts members of?", + "answers": { + "A": "Amphibian", + "B": "Fish", + "C": "Reptiles", + "D": "Mammals" + }, + "correct_letter": "A" +} diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_status_code.json b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_status_code.json new file mode 100644 index 000000000..ae4eef777 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/fixtures/test_status_code.json @@ -0,0 +1,14 @@ +{ + "request": { + "api_url":"https://opentdb.com/api.php?amount=1&type=multiple" + }, + "response": { + "data": { + "status_code":404 + } + }, + "response-headers":{ + "status":404, + "content-type":"text/html" + } +} diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/test_trivia_quiz_game.py b/zulip_bots/zulip_bots/bots/trivia_quiz_game/test_trivia_quiz_game.py new file mode 100644 index 000000000..b4ead5b0e --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/test_trivia_quiz_game.py @@ -0,0 +1,153 @@ +import json + +from unittest.mock import patch + +from zulip_bots.test_lib import ( + BotTestCase, + DefaultTests, + read_bot_fixture_data, +) + +from zulip_bots.request_test_lib import ( + mock_request_exception +) + +from zulip_bots.bots.trivia_quiz_game.controller import ( + TriviaQuizGameModel, + NotAvailableException +) + +from zulip_bots.bots.trivia_quiz_game.trivia_quiz_game import ( + TriviaQuizGameMessageHandler +) + +from zulip_bots.game_handler import BadMoveException + +class TestTriviaQuizGameBot(BotTestCase, DefaultTests): + bot_name = 'trivia_quiz_game' # type: str + + new_question_response = '\nQ: Which class of animals are newts members of?\n\n' + \ + '* **A** Amphibian\n' + \ + '* **B** Fish\n' + \ + '* **C** Reptiles\n' + \ + '* **D** Mammals\n' + \ + '**reply**: ' + + test_question = { + 'question': 'Question 1?', + 'answers': { + 'A': 'Correct', + 'B': 'Incorrect 1', + 'C': 'Incorrect 2', + 'D': 'Incorrect 3' + }, + 'correct_letter': 'A' + } + + test_question_message_content = ''' +Q: Question 1? + +* **A** Correct +* **B** Incorrect 1 +* **C** Incorrect 2 +* **D** Incorrect 3 +**reply**: ''' + + test_question_message_widget = '{"widget_type": "zform", "extra_data": {"type": "choices", "heading": "Question 1?", "choices": [{"type": "multiple_choice", "short_name": "A", "long_name": "Correct", "reply": "A"}, {"type": "multiple_choice", "short_name": "B", "long_name": "Incorrect 1", "reply": "B"}, {"type": "multiple_choice", "short_name": "C", "long_name": "Incorrect 2", "reply": "C"}, {"type": "multiple_choice", "short_name": "D", "long_name": "Incorrect 3", "reply": "D"}]}}' + + def test_question_not_available(self) -> None: + with self.mock_http_conversation('test_new_question'): + model = TriviaQuizGameModel() + # Exception + with self.assertRaises(NotAvailableException): + with mock_request_exception(): + model.get_trivia_quiz() + # non-ok status code + with self.assertRaises(NotAvailableException): + with self.mock_http_conversation("test_status_code"): + model.get_trivia_quiz() + + def test_validate_move(self) -> None: + with self.mock_http_conversation('test_new_question'): + model = TriviaQuizGameModel() + valid_moves = [ + 'A', + 'B', + 'C', + 'D' + ] + invalid_moves = [ + 'AA', + '1' + ] + for valid_move in valid_moves: + self.assertTrue(model.validate_move(valid_move)) + for invalid_move in invalid_moves: + self.assertFalse(model.validate_move(invalid_move)) + + def test_make_move(self) -> None: + with self.mock_http_conversation('test_new_question'): + model = TriviaQuizGameModel() + model.current_board = self.test_question + model.scores = { + 0: 0, + 1: 1 + } + # Invalid move should raise BadMoveException + with self.assertRaises(BadMoveException): + model.make_move('AA', 0) + # Correct move should: + with self.mock_http_conversation('test_new_question'): + with patch('random.shuffle'): + move_data = model.make_move('A', 0) + # Increment score + self.assertEqual(model.scores[0], 1) + # Change question + self.assertEqual(model.current_board, read_bot_fixture_data("trivia_quiz_game", "test_new_question_dict")) + # Move data correct should be true + self.assertTrue(move_data['correct']) + # Move data score should be the same as model.scores[player_number] + self.assertEqual(move_data['score'], 1) + # Incorrect move should: + with self.mock_http_conversation('test_new_question'): + model.current_board = self.test_question + move_data = model.make_move('B', 1) + # Decrement score + self.assertEqual(model.scores[1], 0) + # Move data correct should be false + self.assertFalse(move_data['correct']) + + def test_determine_game_over(self) -> None: + with self.mock_http_conversation('test_new_question'): + model = TriviaQuizGameModel() + model.scores = { + 0: 0, + 1: 5, + 2: 1 + } + self.assertEqual(model.determine_game_over(["Test 0", "Test 1", "Test 2"]), "Test 1") + model.scores = { + 0: 0, + 1: 4, + 2: 1 + } + self.assertIsNone(model.determine_game_over(["Test 0", "Test 1", "Test 2"])) + + def test_message_handler_parse_board(self) -> None: + message_handler = TriviaQuizGameMessageHandler() + board_message_content, board_message_widget = message_handler.parse_board(self.test_question) + self.assertEqual(board_message_content, self.test_question_message_content) + self.assertEqual(json.loads(board_message_widget), json.loads(self.test_question_message_widget)) + + def test_message_handler_alert_move_message(self) -> None: + message_handler = TriviaQuizGameMessageHandler() + correct_responses = [ + (("Test User", "A", {'correct': True, 'score': 5}), ":tada: Correct Test User (5 points)!"), + (("Test User", "B", {'correct': False, 'score': 1, 'correct_letter': "B"}), ":disappointed: Incorrect Test User (1 points). The correct answer was **B**") + ] + for args, response in correct_responses: + self.assertEqual(message_handler.alert_move_message(*args), response) + + def test_message_handler_get_player_color(self) -> None: + message_handler = TriviaQuizGameMessageHandler() + self.assertIsNone(message_handler.get_player_color(0)) diff --git a/zulip_bots/zulip_bots/bots/trivia_quiz_game/trivia_quiz_game.py b/zulip_bots/zulip_bots/bots/trivia_quiz_game/trivia_quiz_game.py new file mode 100644 index 000000000..8ced4ed19 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/trivia_quiz_game/trivia_quiz_game.py @@ -0,0 +1,109 @@ +from typing import Optional, Any, Dict, Tuple, Union +import json + +from zulip_bots.game_handler import GameAdapter +from zulip_bots.bots.trivia_quiz_game.controller import TriviaQuizGameModel + +class TriviaQuizGameMessageHandler(object): + def parse_board(self, question: Dict[str, Any]) -> Union[Tuple[str, str], str]: + bot_response = self.format_quiz_for_markdown(question) + widget_content = self.format_quiz_for_widget(question) + return bot_response, widget_content + + def format_quiz_for_widget(self, question: Dict[str, Any]) -> str: + widget_type = 'zform' + question_str = question['question'] + answers = question['answers'] + + heading = question_str + + def get_choice(letter: str) -> Dict[str, str]: + answer = answers[letter] + reply = letter + + return dict( + type='multiple_choice', + short_name=letter, + long_name=answer, + reply=reply, + ) + + choices = [get_choice(letter) for letter in 'ABCD'] + + extra_data = dict( + type='choices', + heading=heading, + choices=choices, + ) + + widget_content = dict( + widget_type=widget_type, + extra_data=extra_data, + ) + payload = json.dumps(widget_content) + return payload + + def format_quiz_for_markdown(self, question: Dict[str, Any]) -> str: + question_str = question['question'] + answers = question['answers'] + answer_list = '\n'.join([ + '* **{letter}** {answer}'.format( + letter=letter, + answer=answers[letter], + ) + for letter in 'ABCD' + ]) + how_to_respond = '''**reply**: ''' + + content = ''' +Q: {question} + +{answer_list} +{how_to_respond}'''.format( + question=question_str, + answer_list=answer_list, + how_to_respond=how_to_respond, + ) + return content + + def get_player_color(self, turn: int) -> Optional[str]: + # There is no player colour, players don't play with tokens + return None + + def alert_move_message(self, original_player: str, move_info: str, move_data: Any = None) -> str: + if move_data['correct']: + return ":tada: Correct {} ({} points)!".format(original_player, move_data['score']) + return ":disappointed: Incorrect {} ({} points). The correct answer was **{}**".format( + original_player, move_data['score'], move_data['correct_letter']) + + def game_start_message(self) -> str: + return 'Answer the questions correctly, and try to get the most points!\n Good luck!' + +class TriviaQuizGameBotHandler(GameAdapter): + ''' + Bot that uses the Game Adapter class + to allow users to play other users + or the computer in a Trivia Game + ''' + def __init__(self) -> None: + game_name = 'Trivia' + bot_name = 'trivia_quiz' + move_help_message = '* To answer a question, click an answer button' + move_regex = '([ABCD])' + model = TriviaQuizGameModel + gameMessageHandler = TriviaQuizGameMessageHandler + rules = 'Get the most points in the quiz!' + super(TriviaQuizGameBotHandler, self).__init__( + game_name, + bot_name, + move_help_message, + move_regex, + model, + gameMessageHandler, + rules, + max_players=4, + min_players=1, + supports_computer=False + ) + +handler_class = TriviaQuizGameBotHandler diff --git a/zulip_bots/zulip_bots/game_handler.py b/zulip_bots/zulip_bots/game_handler.py index 8e22a9a56..e2924f0fb 100644 --- a/zulip_bots/zulip_bots/game_handler.py +++ b/zulip_bots/zulip_bots/game_handler.py @@ -3,7 +3,7 @@ import random import logging from copy import deepcopy -from typing import Any, Dict, Tuple, List +from typing import Any, Dict, Tuple, List, Union, Optional class BadMoveException(Exception): @@ -152,16 +152,21 @@ def confirm_invitation_declined(self, game_id: str) -> str: host = self.invites[game_id]['host'] return 'Declined invitation to play **{}** from @**{}**.'.format(self.game_name, self.get_username_by_email(host)) - def send_message(self, to: str, content: str, is_private: bool, subject: str = '') -> None: + def send_message(self, to: str, content: str, is_private: bool, subject: str = '', widget_content: Optional[str] = '') -> None: + if widget_content == '': + widget_content = None self.bot_handler.send_message(dict( type='private' if is_private else 'stream', to=to, content=content, - subject=subject + subject=subject, + widget_content=widget_content )) - def send_reply(self, original_message: Dict[str, Any], content: str) -> None: - self.bot_handler.send_reply(original_message, content) + def send_reply(self, original_message: Dict[str, Any], content: str, widget_content: Optional[str] = '') -> None: + if widget_content == '': + widget_content = None + self.bot_handler.send_reply(original_message, content, widget_content) def usage(self) -> str: return ''' @@ -475,8 +480,14 @@ def start_game(self, game_id: str) -> None: stream = self.invites[game_id]['stream'] if self.invites[game_id]['subject'] != '###private###': subject = self.invites[game_id]['subject'] - self.instances[game_id] = GameInstance( - self, False, subject, game_id, players, stream) + try: + self.instances[game_id] = GameInstance( + self, False, subject, game_id, players, stream) + except Exception as e: + # There was an error in creating the game. End the game + logging.exception(str(e)) + self.cancel_game(game_id, "An error occured while creating the game\n{}".format(str(e))) + return self.broadcast(game_id, 'The game has started in #{} {}'.format( stream, self.instances[game_id].subject) + '\n' + self.get_formatted_game_object(game_id)) del self.invites[game_id] @@ -579,8 +590,12 @@ def parse_message(self, message: Dict[str, Any]) -> None: self.pending_subject_changes.remove(game_id) self.change_game_subject( game_id, message['display_recipient'], message['subject'], message) - self.instances[game_id].handle_message( - message['content'], message['sender_email']) + try: + self.instances[game_id].handle_message( + message['content'], message['sender_email']) + except Exception as e: + logging.exception(str(e)) + self.cancel_game(game_id, "An error occured in GameInstance.\n{}".format(str(e))) def change_game_subject( self, @@ -710,20 +725,20 @@ def generate_game_id(self) -> str: id += valid_characters[random.randrange(0, len(valid_characters))] return id - def broadcast(self, game_id: str, content: str, include_private: bool = True) -> bool: + def broadcast(self, game_id: str, content: str, include_private: bool = True, widget_content: str = '') -> bool: if include_private: private_recipients = self.get_players(game_id, parameter='p') if private_recipients is not None: for user in private_recipients: - self.send_message(user, content, True) + self.send_message(user, content, True, '', widget_content) if game_id in self.invites.keys(): if self.invites[game_id]['subject'] != '###private###': self.send_message( - self.invites[game_id]['stream'], content, False, self.invites[game_id]['subject']) + self.invites[game_id]['stream'], content, False, self.invites[game_id]['subject'], widget_content) return True if game_id in self.instances.keys(): self.send_message( - self.instances[game_id].stream, content, False, self.instances[game_id].subject) + self.instances[game_id].stream, content, False, self.instances[game_id].subject, widget_content) return True return False @@ -771,7 +786,7 @@ def __init__(self, gameAdapter: GameAdapter, is_private: bool, subject: str, gam self.board = self.model.current_board self.turn = random.randrange(0, len(players)) - 1 self.current_draw = {} # type: Dict[str, bool] - self.current_messages = [] # type: List[str] + self.current_messages = [] # type: List[Union[str, Tuple[str, str]]] self.is_changing_subject = False def start(self) -> None: @@ -822,18 +837,28 @@ def handle_message(self, content: str, player_email: str) -> None: if self.is_turn_of(player_email): self.handle_current_player_command(content) else: - if self.gameAdapter.is_single_player: - self.broadcast('It\'s your turn') + self.send_current_turn_message() + self.broadcast_current_message() + + def send_current_turn_message(self) -> None: + if self.gameAdapter.is_single_player: + self.current_messages.append('It\'s your turn') + else: + user_turn_avatar = "!avatar({})".format(self.players[self.turn]) + if self.gameAdapter.gameMessageHandler.get_player_color(self.turn) is None: + self.current_messages.append('{} It\'s **{}**\'s turn.'.format( + user_turn_avatar, + self.gameAdapter.get_username_by_email( + self.players[self.turn]))) else: - user_turn_avatar = "!avatar({})".format(self.players[self.turn]) - self.broadcast('{} It\'s **{}**\'s ({}) turn.'.format( + self.current_messages.append('{} It\'s **{}**\'s ({}) turn.'.format( user_turn_avatar, self.gameAdapter.get_username_by_email( self.players[self.turn]), self.gameAdapter.gameMessageHandler.get_player_color(self.turn))) - def broadcast(self, content: str) -> None: - self.gameAdapter.broadcast(self.game_id, content) + def broadcast(self, content: str, widget_content: str = '') -> None: + self.gameAdapter.broadcast(self.game_id, content, widget_content=widget_content) def check_draw(self) -> bool: for d in self.current_draw.values(): @@ -850,18 +875,22 @@ def handle_current_player_command(self, content: str) -> None: def make_move(self, content: str, is_computer: bool) -> None: try: - self.model.make_move(content, self.turn, is_computer) + move_data = self.model.make_move(content, self.turn, is_computer) # Keep the turn of the same player except SamePlayerMove as smp: self.same_player_turn(content, smp.message, is_computer) return except BadMoveException as e: self.broadcast(e.message) - self.broadcast(self.parse_current_board()) + current_board_message = self.parse_current_board() + if isinstance(current_board_message, tuple): + self.broadcast( + current_board_message[0], current_board_message[1]) + self.broadcast(str(current_board_message)) return if not is_computer: self.current_messages.append(self.gameAdapter.gameMessageHandler.alert_move_message( - '**{}**'.format(self.gameAdapter.get_username_by_email(self.players[self.turn])), content)) + '**{}**'.format(self.gameAdapter.get_username_by_email(self.players[self.turn])), content, move_data)) self.current_messages.append(self.parse_current_board()) game_over = self.model.determine_game_over(self.players) if game_over: @@ -889,12 +918,7 @@ def same_player_turn(self, content: str, message: str, is_computer: bool) -> Non game_over = self.players[self.turn] self.end_game(game_over) return - user_turn_avatar = "!avatar({})".format(self.players[self.turn]) - self.current_messages.append('{} It\'s **{}**\'s ({}) turn.'.format( - user_turn_avatar, - self.gameAdapter.get_username_by_email(self.players[self.turn]), - self.gameAdapter.gameMessageHandler.get_player_color(self.turn) - )) + self.send_current_turn_message() self.broadcast_current_message() if self.players[self.turn] == self.gameAdapter.email: self.make_move('', True) @@ -903,22 +927,21 @@ def next_turn(self) -> None: self.turn += 1 if self.turn >= len(self.players): self.turn = 0 - if self.gameAdapter.is_single_player: - self.current_messages.append('It\'s your turn.') - else: - user_turn_avatar = "!avatar({})".format(self.players[self.turn]) - self.current_messages.append('{} It\'s **{}**\'s ({}) turn.'.format( - user_turn_avatar, - self.gameAdapter.get_username_by_email(self.players[self.turn]), - self.gameAdapter.gameMessageHandler.get_player_color(self.turn) - )) + self.send_current_turn_message() self.broadcast_current_message() if self.players[self.turn] == self.gameAdapter.email: self.make_move('', True) def broadcast_current_message(self) -> None: - content = '\n\n'.join(self.current_messages) - self.broadcast(content) + # if there are no widgets, send all the messages all at once + if len(list(filter(lambda x: isinstance(x, tuple), self.current_messages))) == 0: + self.broadcast("\n\n".join(str(m) for m in self.current_messages)) + else: + for message in self.current_messages: + if isinstance(message, tuple): + self.broadcast(message[0], message[1]) + else: + self.broadcast(str(message)) self.current_messages = [] def parse_current_board(self) -> Any: