Game Adapter Class: Allow superusers to moderate bots.

This commit is contained in:
sColin16 2017-12-24 23:54:44 +00:00 committed by showell
parent 2c8b369d14
commit b9905e5fc8
6 changed files with 62 additions and 29 deletions

View file

@ -0,0 +1,2 @@
[connect_four]
superusers = ["user@example.com"]

View file

@ -3,7 +3,7 @@ from zulip_bots.bots.connect_four.controller import ConnectFourModel
class ConnectFourMessageHandler(object): class ConnectFourMessageHandler(object):
tokens = [':blue_circle:', ':red_circle:'] tokens = [':blue_circle:', ':red_circle:']
def parse_board(self, board): def parse_board(self, board):
# Header for the top of the board # Header for the top of the board
board_str = ':one: :two: :three: :four: :five: :six: :seven:' board_str = ':one: :two: :three: :four: :five: :six: :seven:'
@ -19,7 +19,7 @@ class ConnectFourMessageHandler(object):
board_str += ':red_circle: ' board_str += ':red_circle: '
return board_str return board_str
def get_player_color(self, turn): def get_player_color(self, turn):
return self.tokens[turn] return self.tokens[turn]
@ -41,16 +41,16 @@ class ConnectFourBotHandler(GameAdapter):
or the comptuer in a game of Connect or the comptuer in a game of Connect
Four Four
''' '''
def __init__(self): def __init__(self):
game_name = 'Connect Four' game_name = 'Connect Four'
bot_name = 'connect_four' bot_name = 'connect_four'
move_help_message = '* To make your move during a game, type\n' + \ move_help_message = '* To make your move during a game, type\n' \
'```move <column-number>```' '```move <column-number>```'
move_regex = 'move (\d)$' move_regex = 'move (\d)$'
model = ConnectFourModel model = ConnectFourModel
gameMessageHandler = ConnectFourMessageHandler gameMessageHandler = ConnectFourMessageHandler
super(ConnectFourBotHandler, self).__init__(game_name, bot_name, move_help_message, move_regex, model, gameMessageHandler) super(ConnectFourBotHandler, self).__init__(game_name, bot_name, move_help_message, move_regex, model, gameMessageHandler)
handler_class = ConnectFourBotHandler handler_class = ConnectFourBotHandler

View file

@ -17,10 +17,10 @@ class ConnectFourModel(object):
[0, 0, 0, 0, 0, 0, 0]] [0, 0, 0, 0, 0, 0, 0]]
current_board = blank_board current_board = blank_board
def parse_move(self, move): def parse_move(self, move):
return int(move) - 1 return int(move) - 1
def update_board(self, board): def update_board(self, board):
self.current_board = deepcopy(board) self.current_board = deepcopy(board)

View file

@ -25,8 +25,12 @@ can only run a single game at a time*
## Setup ## Setup
The Connect Four Bot does not require a config file or API key. To set moderators for the bot, modify the connect_four.conf
It can be used without setup. file as shown:
superusers = ["user@example.com", "user@example2.com", ...]
Moderators can run ```force reset``` in case any user abuse the bot
## Usage ## Usage
@ -61,7 +65,7 @@ another user.
player to decline an invitation to play Connect Four against player to decline an invitation to play Connect Four against
another user. another user.
7. ```withdraw invitation``` : a command that can only be run by the 7. ```cancel game``` : a command that can only be run by the
inviter to withdraw their invitation to play. Especially inviter to withdraw their invitation to play. Especially
useful if a player does not respond to an invitation for a useful if a player does not respond to an invitation for a
long period of time. long period of time.
@ -75,3 +79,7 @@ the user to confirm they wish to forfeit the game.
10. ```confirm quit``` : causes the user that runs this command 10. ```confirm quit``` : causes the user that runs this command
to forfeit the game. to forfeit the game.
11. ```force reset``` : a command that can only be run by the bot
owner and moderators (see 'Usage' for specifying). Destroys any
game currently being run if users are abusing the bot

View file

@ -1,11 +1,9 @@
# @TODO: place bot owner name in config file, allow bot owner to run special commands
import re import re
import json
from copy import deepcopy from copy import deepcopy
# @TODO: allow superusers
class InputVerification(object): class InputVerification(object):
def __init__(self, move_regex): def __init__(self, move_regex, superusers):
self.move_regex = move_regex self.move_regex = move_regex
self.verified_commands = { self.verified_commands = {
'waiting': ['start game with computer', 'start game with \w+@\w+\.\w+'], 'waiting': ['start game with computer', 'start game with \w+@\w+\.\w+'],
@ -13,17 +11,16 @@ class InputVerification(object):
'playing': [[move_regex, 'quit', 'confirm quit'], ['quit', 'confirm quit']] 'playing': [[move_regex, 'quit', 'confirm quit'], ['quit', 'confirm quit']]
} }
self.all_valid_commands = ['help', 'status', 'start game with computer', 'start game with \w+@\w+\.\w+', self.all_valid_commands = ['help', 'status', 'start game with computer', 'start game with \w+@\w+\.\w+',
'withdraw invitation', 'accept', 'decline', self.move_regex, 'quit', 'confirm quit'] 'withdraw invitation', 'accept', 'decline', self.move_regex, 'quit', 'confirm quit', 'force reset']
self.superusers = superusers
verified_users = []
verified_users = []
def permission_lacking_message(self, command): def permission_lacking_message(self, command):
return 'Sorry, but you can\'t run the command ```' + command + '```' return 'Sorry, but you can\'t run the command ```' + command + '```'
def update_commands(self, turn): def update_commands(self, turn):
self.verified_commands['playing'][-1 * turn + 1].remove(self.move_regex) self.verified_commands['playing'] = [['quit', 'confirm quit'], ['quit', 'confirm quit']]
self.verified_commands['playing'][turn].append(self.move_regex) self.verified_commands['playing'][turn].append(self.move_regex)
def reset_commands(self): def reset_commands(self):
@ -50,6 +47,9 @@ class InputVerification(object):
return self.regex_match_in_array(command_array, command) return self.regex_match_in_array(command_array, command)
def verify_superuser(self, user):
return user in self.superusers
class StateManager(object): class StateManager(object):
def __init__(self, main_bot_handler): def __init__(self, main_bot_handler):
self.users = None self.users = None
@ -331,11 +331,9 @@ class GameAdapter(object):
self.move_help_message = move_help_message self.move_help_message = move_help_message
self.model = model self.model = model
self.gameMessageHandler = gameMessageHandler() self.gameMessageHandler = gameMessageHandler()
self.inputVerification = InputVerification(move_regex) self.inputVerification = InputVerification(move_regex, [])
def get_stored_data(self): def get_stored_data(self):
# @TODO: remove this comment when you create super users
# return self.data # Uncomment and rerun bot to reset data if users are abusing the bot
return self.bot_handler.storage.get(self.bot_name) return self.bot_handler.storage.get(self.bot_name)
def update_data(self): def update_data(self):
@ -343,13 +341,17 @@ class GameAdapter(object):
if 'users' in self.data: if 'users' in self.data:
self.inputVerification.verified_users = self.data['users'] self.inputVerification.verified_users = self.data['users']
else:
self.inputVerification.verified_users = []
if self.state == 'inviting': if self.state == 'inviting':
self.invitationHandler = InvitationHandler(self) self.invitationHandler = InvitationHandler(self)
self.gameHandler = GameHandler(self, self.data['game_type'], self.model()) self.gameHandler = GameHandler(self, self.data['game_type'], self.model())
elif self.state == 'playing': elif self.state == 'playing':
self.gameHandler = GameHandler(self, self.data['game_type'], self.model(), board = self.data['board'], turn = self.data['turn'], ) self.gameHandler = GameHandler(self, self.data['game_type'], self.model(),
board = self.data['board'], turn = self.data['turn'])
self.inputVerification.update_commands(self.data['turn'])
def put_stored_data(self): def put_stored_data(self):
self.data = {} self.data = {}
@ -470,11 +472,26 @@ class GameAdapter(object):
''' '''
def initialize(self, bot_handler): def initialize(self, bot_handler):
self.config_info = bot_handler.get_config_info('connect_four')
if self.config_info:
self.inputVerification.superusers = json.loads(self.config_info['superusers'])
self.gameCreator = GameCreator(self) self.gameCreator = GameCreator(self)
self.inputVerification.reset_commands() self.inputVerification.reset_commands()
if not bot_handler.storage.contains(self.bot_name): if not bot_handler.storage.contains(self.bot_name):
bot_handler.storage.put(self.bot_name, self.data) bot_handler.storage.put(self.bot_name, self.data)
def force_reset(self, sender):
for user in self.inputVerification.verified_users:
self.send_message(user, 'A bot moderator determined you were abusing the bot, and quit your game.'
' Please make sure you finish all your games in a timely fashion.')
self.send_message(sender, 'The game has been force reset')
self.data = data = {'state': 'waiting'}
self.update_data()
self.put_stored_data()
def handle_message(self, message, bot_handler): def handle_message(self, message, bot_handler):
self.bot_handler = bot_handler self.bot_handler = bot_handler
@ -489,7 +506,10 @@ class GameAdapter(object):
'Type ```help``` to see a full list of commands.') 'Type ```help``` to see a full list of commands.')
return return
# Messages that can be sent regardless of state or user elif self.inputVerification.verify_superuser(sender) and content.lower() == 'force reset':
self.force_reset(sender)
return
elif content.lower() == 'help' or content == '': elif content.lower() == 'help' or content == '':
self.send_message(sender, self.help_message()) self.send_message(sender, self.help_message())
return return
@ -524,4 +544,4 @@ class GameAdapter(object):
self.gameHandler.handle_message(content, sender) self.gameHandler.handle_message(content, sender)
self.send_message_arrays(sender) self.send_message_arrays(sender)
self.put_stored_data() self.put_stored_data()

View file

@ -137,7 +137,7 @@ class TestConnectFourBot(BotTestCase):
def test_move(self): def test_move(self):
self.verify_response('move 8', 'That\'s an invalid move. Please specify a column ' self.verify_response('move 8', 'That\'s an invalid move. Please specify a column '
'between 1 and 7 with at least one open spot.', 0, data=self.start_two_player_data) 'between 1 and 7 with at least one open spot.', 0, data=self.start_two_player_data)
self.verify_response('move 1', 'You placed your token in column 1.', 0, data=self.start_two_player_data) self.verify_response('move 1', 'You placed your token in column 1.', 0, data=self.start_two_player_data)
self.verify_response('move 1', '**the Computer moved in column 1**.', 3, data=self.start_one_player_data, computer_move=0) self.verify_response('move 1', '**the Computer moved in column 1**.', 3, data=self.start_one_player_data, computer_move=0)
@ -149,16 +149,19 @@ class TestConnectFourBot(BotTestCase):
def test_quit(self): def test_quit(self):
self.verify_response('quit', 'Are you sure you want to quit? You will forfeit the game!\n' self.verify_response('quit', 'Are you sure you want to quit? You will forfeit the game!\n'
'Type ```confirm quit``` to forfeit.', 0, data=self.start_two_player_data) 'Type ```confirm quit``` to forfeit.', 0, data=self.start_two_player_data)
def test_confirm_quit(self):
self.verify_response('confirm quit', '**You have forfeit the game**\nSorry, but you lost :cry:', 0, data=self.start_two_player_data) self.verify_response('confirm quit', '**You have forfeit the game**\nSorry, but you lost :cry:', 0, data=self.start_two_player_data)
def test_force_reset(self):
with self.mock_config_info({'superusers': '["foo@example.com"]'}):
self.verify_response('force reset', 'The game has been force reset', 1, data=self.start_one_player_data)
def test_privilege_check(self): def test_privilege_check(self):
self.verify_response('move 4', 'Sorry, but you can\'t run the command ```move 4```', 0, data=self.inviting_two_player_data) self.verify_response('move 4', 'Sorry, but you can\'t run the command ```move 4```', 0, data=self.inviting_two_player_data)
self.verify_response('start game with computer', 'Sorry, but other users are already using the bot.' self.verify_response('start game with computer', 'Sorry, but other users are already using the bot.'
'Type ```status``` to see the current status of the bot.', 0, data=self.inviting_two_player_data, user = 'foo3@example.com') 'Type ```status``` to see the current status of the bot.', 0, data=self.inviting_two_player_data, user = 'foo3@example.com')
self.verify_response('quit', 'Sorry, but you can\'t run the command ```quit```', 0) self.verify_response('quit', 'Sorry, but you can\'t run the command ```quit```', 0)
self.verify_response('accept', 'Sorry, but you can\'t run the command ```accept```', 0, data=self.end_two_player_data) self.verify_response('accept', 'Sorry, but you can\'t run the command ```accept```', 0, data=self.end_two_player_data)
self.verify_response('force reset', 'Sorry, but you can\'t run the command ```force reset```', 0)
def test_connect_four_logic(self): def test_connect_four_logic(self):
def confirmAvailableMoves(good_moves, bad_moves, board): def confirmAvailableMoves(good_moves, bad_moves, board):