From 786821e9b086bd8ecf7b6a795301281dd6ed00ac Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 13:32:58 -0500 Subject: [PATCH 01/23] Testing story mode --- bots/story.py | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 bots/story.py diff --git a/bots/story.py b/bots/story.py new file mode 100644 index 0000000..2182add --- /dev/null +++ b/bots/story.py @@ -0,0 +1,195 @@ +from .core import CoreBot +from .utils import getname, load_db, save_db +import discord +import asyncio +import os +import subprocess +import queue +import threading +import re + +more_patterns = [ + re.compile(r'\*+(MORE|more)\*+') +] + +score_patterns = [ + re.compile(r'([0-9]+)/[0-9+]'), + re.compile(r'Score:[ ]*([-]*[0-9]+)'), + re.compile(r'([0-9]+):[0-9]+ [AaPp][Mm]') +] + +clean_patterns = [ + # re.compile(r'[0-9]+/[0-9+]'), + # re.compile(r'Score:[ ]*[-]*[0-9]+'), + re.compile(r'Moves:[ ]*[0-9]+'), + re.compile(r'Turns:[ ]*[0-9]+'), + # re.compile(r'[0-9]+:[0-9]+ [AaPp][Mm]'), + re.compile(r' [0-9]+ \.') +] + more_patterns + score_patterns + +def multimatch(text, patterns): + for pattern in patterns: + result = pattern.search(text) + if result: + return result + return False + +class Player: + def __init__(self, game): + (self.stdinRead, self.stdinWrite) = os.pipe() + (self.stdoutRead, self.stdoutWrite) = os.pipe() + self.buffer = queue.Queue() + self.remainder = b'' + self.score = 0 + self.proc = subprocess.Popen( + 'dfrotz games/%s.z5' % game, + universal_newlines=False, + shell=True, + stdout=self.stdoutWrite, + stdin=self.stdinRead + ) + self._reader = threading.Thread( + target=Player.reader, + args=(self,), + daemon=True, + ) + self._reader.start() + + def write(self, text): + if not text.endswith('\n'): + text+='\n' + os.write(self.stdinWrite, text.encode()) + + def reader(self): + while True: + self.buffer.put(self.readline()) + + def readline(self): + intake = self.remainder + while b'\n' not in intake: + intake += os.read(self.stdoutRead, 64) + lines = intake.split(b'\n') + self.remainder = b'\n'.join(lines[1:]) + return lines[0].decode().rstrip() + + def readchunk(self, clean=True): + content = [self.buffer.get()] + try: + while not self.buffer.empty(): + content.append(self.buffer.get(timeout=0.5)) + except queue.Empty: + pass + + # clean metadata + if multimatch(content[-1], more_patterns): + self.write('\n') + content += self.readchunk(False) + + if clean: + for i in range(len(content)): + line = content[i] + result = multimatch(line, score_patterns) + if result: + self.score = int(result.group(1)) + result = multimatch(line, clean_patterns) + while result: + line = result.re.sub('', line) + result = multimatch(line, clean_patterns) + content[i] = line + return '\n'.join(line for line in content if len(line.rstrip())) + + def quit(self): + self.write('quit') + self.write('y') + try: + self.proc.wait(1) + except: + self.proc.kill() + os.close(self.stdinRead) + os.close(self.stdinWrite) + os.close(self.stdoutRead) + os.close(self.stdoutWrite) + + +def EnableStory(bot): + if not isinstance(bot, CoreBot): + raise TypeError("This function must take a CoreBot") + + @bot.add_command('!_stories') + def cmd_story(self, message, content): + games = [ + f[:-3] for f in os.listdir('games') if f.endswith('.z5') + ] + await self.send_message( + message.channel, + '\n'.join( + "Here are the stories thar are available:", + *games + ) + ) + + def checker(self, message): + state = load_db('game.json', {'user':'~'}) + return state['user'] != '~' + + @bot.add_special(checker) + def state_router(self, message, content): + # Routes messages depending on the game state + state = load_db('game.json', {'user':'~'}) + if state['user'] == message.author.id: + content = message.content.strip().lower() + if content == '$': + content = '\n' + self.player.write('\n') + elif content == '$quit': + self.player.quit() + await self.send_message( + message.channel, + 'You have quit your game.' + ) + state['user'] = '~' + del self.player + save_db(state, 'game.json') + else: + self.player.write(content) + await self.send_message( + message.channel, + self.player.readchunk() + ) + else: + await self.send_message( + message.author, + "Please refrain from posting messages in the story channel" + " while someone else is playing" + ) + + @bod.add_command('!_start') + def cmd_start(self, message, content): + state = load_db('game.json', {'user':'~'}) + if state['user'] != '~': + games = { + f[:-3] for f in os.listdir('games') if f.endswith('.z5') + } + if content[1] in games: + state['user'] = message.author.id + save_db(state, 'game.json') + self.player = Player(content[1]) + # in future: + # 1) use a reserved channel so other users can watch + # 2) Post to general that a game is starting + # 3) Mention in game channel to get user's attention + # 4) Lock permissions in game channel to prevent non-players from posting messages + await self.send_message( + message.channel, + self.player.readchunk() + ) + else: + await self.send_message( + message.channel, + "That is not a valid game" + ) + else: + await self.send_message( + message.channel, + "Please wait until the current player finishes their game" + ) From adfb144b2b5e2550158a289b621a5aa469d6831e Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 19:41:05 +0000 Subject: [PATCH 02/23] Added basic story implimentation --- .gitignore | 4 +++ bots/story.py | 72 +++++++++++++++++++++++++++++++++++++++++---------- main.py | 4 ++- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 554a137..ac5fbb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +dfrotz +games/ +textplayer/ +git.token token.txt _token.txt permissions.yml diff --git a/bots/story.py b/bots/story.py index 2182add..1bafa4d 100644 --- a/bots/story.py +++ b/bots/story.py @@ -42,7 +42,7 @@ def __init__(self, game): self.remainder = b'' self.score = 0 self.proc = subprocess.Popen( - 'dfrotz games/%s.z5' % game, + './dfrotz games/%s.z5' % game, universal_newlines=False, shell=True, stdout=self.stdoutWrite, @@ -116,45 +116,70 @@ def EnableStory(bot): raise TypeError("This function must take a CoreBot") @bot.add_command('!_stories') - def cmd_story(self, message, content): + async def cmd_story(self, message, content): games = [ f[:-3] for f in os.listdir('games') if f.endswith('.z5') ] await self.send_message( message.channel, '\n'.join( - "Here are the stories thar are available:", - *games + ["Here are the stories thar are available:"]+ + games ) ) def checker(self, message): state = load_db('game.json', {'user':'~'}) - return state['user'] != '~' + return state['user'] != '~' and not message.content.startswith('!') @bot.add_special(checker) - def state_router(self, message, content): + async def state_router(self, message, content): # Routes messages depending on the game state state = load_db('game.json', {'user':'~'}) if state['user'] == message.author.id: + if not hasattr(self, 'player'): + # The game has been interrupted + await self.send_message( + message.channel, + "Resuming game in progress...\n" + "Please wait" + ) + self.player = Player(state['game']) + for msg in state['transcript']: + self.player.write(msg) + await asyncio.sleep(0.5) + self.player.readchunk() content = message.content.strip().lower() if content == '$': content = '\n' + state['transcript'].append(content) + save_db(state, 'game.json') self.player.write('\n') - elif content == '$quit': + await self.send_message( + message.channel, + '```'+self.player.readchunk()+'```' + ) + elif content == 'score': + await self.send_message( + message.channel, + 'Your score is %d' % self.player.score + ) + elif content == 'quit': self.player.quit() await self.send_message( message.channel, - 'You have quit your game.' + 'You have quit your game. Your score was %d' % self.player.score ) state['user'] = '~' del self.player save_db(state, 'game.json') else: + state['transcript'].append(content) + save_db(state, 'game.json') self.player.write(content) await self.send_message( message.channel, - self.player.readchunk() + '```'+self.player.readchunk()+'```' ) else: await self.send_message( @@ -163,15 +188,17 @@ def state_router(self, message, content): " while someone else is playing" ) - @bod.add_command('!_start') - def cmd_start(self, message, content): + @bot.add_command('!_start') + async def cmd_start(self, message, content): state = load_db('game.json', {'user':'~'}) - if state['user'] != '~': + if state['user'] == '~': games = { f[:-3] for f in os.listdir('games') if f.endswith('.z5') } if content[1] in games: state['user'] = message.author.id + state['transcript'] = [] + state['game'] = content[1] save_db(state, 'game.json') self.player = Player(content[1]) # in future: @@ -179,9 +206,28 @@ def cmd_start(self, message, content): # 2) Post to general that a game is starting # 3) Mention in game channel to get user's attention # 4) Lock permissions in game channel to prevent non-players from posting messages + await self.send_message( + message.author, + 'Here are the controls for the story-mode system:\n' + 'Any message you type in the story channel will be interpreted' + ' as input to the game *unless* your message starts with `!`' + ' (discord commands) or `$` (story commands)\n' + '`$` : Simply type `$` to enter a blank line to the game\n' + '`quit` : Quits the game in progress\n' + '`score` : View your score' + ) await self.send_message( message.channel, - self.player.readchunk() + '%s is now playing %s\n' + 'The game will begin shortly' % ( + message.author.mention, + content[1] + ) + ) + await asyncio.sleep(2) + await self.send_message( + message.channel, + '```'+self.player.readchunk()+'```' ) else: await self.send_message( diff --git a/main.py b/main.py index 43cd963..f33bdcc 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from bots.party import EnableParties from bots.poll import EnablePolls from bots.cash import EnableCash +from bots.story import EnableStory import discord import asyncio import random @@ -130,7 +131,8 @@ async def react(self, message, content): EnableOverwatch, EnableParties, EnablePolls, - EnableCash + EnableCash, + EnableStory ) return beymax From 678e22a23d891cf6ed0ef88b9c59c4f6d0fafff6 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 20:16:54 +0000 Subject: [PATCH 03/23] Story mode gets priority on message recognition --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index f33bdcc..b42f77f 100644 --- a/main.py +++ b/main.py @@ -81,6 +81,8 @@ async def on_member_join(self, member): #greet new members def ConstructBeymax(): #enable Beymax-Specific commands beymax = Beymax() + beymax = EnableStory(beymax) # Story needs priority on special message recognition + @beymax.add_command('!kill-beymax', '!satisfied') async def cmd_shutdown(self, message, content): """ @@ -132,7 +134,6 @@ async def react(self, message, content): EnableParties, EnablePolls, EnableCash, - EnableStory ) return beymax From 45af9db5acd86708ee3c52e4dd355d3aef247389 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 15:24:08 -0500 Subject: [PATCH 04/23] Added separate story channel --- bots/core.py | 4 ++++ bots/story.py | 10 ++++++---- main.py | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bots/core.py b/bots/core.py index fbd273a..97ba953 100644 --- a/bots/core.py +++ b/bots/core.py @@ -58,6 +58,8 @@ async def on_ready(self): self._bug_channel = self._general #Change which channels these use self.bug_channel = self._general #Change which channels these use self.dev_channel = self._general #Change which channels these use + self._story_channel = self._general #Change which channels these use + self.story_channel = self._general #Change which channels these use self.primary_server = self._general.server self.update_times = [0] * len(self.tasks) # set all tasks to update at next trigger self.permissions = None @@ -282,6 +284,7 @@ async def cmd_dev(self, message, content): """ self.general = self.dev_channel self.bug_channel = self.dev_channel + self.story_channel = self.dev_channel await self.send_message( self.dev_channel, "Development mode enabled. All messages will be sent to testing grounds" @@ -294,6 +297,7 @@ async def cmd_prod(self, message, content): """ self.general = self._general self.bug_channel = self._bug_channel + self.story_channel = self._story_channel await self.send_message( self.dev_channel, "Production mode enabled. All messages will be sent to general" diff --git a/bots/story.py b/bots/story.py index 1bafa4d..5e55fc6 100644 --- a/bots/story.py +++ b/bots/story.py @@ -130,7 +130,7 @@ async def cmd_story(self, message, content): def checker(self, message): state = load_db('game.json', {'user':'~'}) - return state['user'] != '~' and not message.content.startswith('!') + return message.channel.id == self.story_channel.id and state['user'] != '~' and not message.content.startswith('!') @bot.add_special(checker) async def state_router(self, message, content): @@ -214,10 +214,12 @@ async def cmd_start(self, message, content): ' (discord commands) or `$` (story commands)\n' '`$` : Simply type `$` to enter a blank line to the game\n' '`quit` : Quits the game in progress\n' - '`score` : View your score' + '`score` : View your score\n' + 'Some games may have their own commands in addition to these' + ' ones that I handle personally' ) await self.send_message( - message.channel, + self.story_channel, '%s is now playing %s\n' 'The game will begin shortly' % ( message.author.mention, @@ -226,7 +228,7 @@ async def cmd_start(self, message, content): ) await asyncio.sleep(2) await self.send_message( - message.channel, + self.story_channel, '```'+self.player.readchunk()+'```' ) else: diff --git a/main.py b/main.py index b42f77f..078306b 100644 --- a/main.py +++ b/main.py @@ -69,6 +69,12 @@ async def on_ready(self): type=discord.ChannelType.text ) self.bug_channel = self._bug_channel + self._story_channel = discord.utils.get( + self.get_all_channels(), + name='secret_channel', + type=discord.ChannelType.text + ) + self.story_channel = self._story_channel print("Ready to serve!") async def on_member_join(self, member): #greet new members From eb093ca060246a7ecd6f043d50a81ae5eff525d4 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 20:31:51 +0000 Subject: [PATCH 05/23] Beymax will now delete other messages in the channel --- bots/story.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bots/story.py b/bots/story.py index 5e55fc6..89f351c 100644 --- a/bots/story.py +++ b/bots/story.py @@ -182,6 +182,7 @@ async def state_router(self, message, content): '```'+self.player.readchunk()+'```' ) else: + await self.delete_message(message) await self.send_message( message.author, "Please refrain from posting messages in the story channel" @@ -202,10 +203,8 @@ async def cmd_start(self, message, content): save_db(state, 'game.json') self.player = Player(content[1]) # in future: - # 1) use a reserved channel so other users can watch - # 2) Post to general that a game is starting - # 3) Mention in game channel to get user's attention - # 4) Lock permissions in game channel to prevent non-players from posting messages + # See if there's a way to change permissions of an existing channel + # For now, just delete other player's messages await self.send_message( message.author, 'Here are the controls for the story-mode system:\n' @@ -226,6 +225,7 @@ async def cmd_start(self, message, content): content[1] ) ) + # Post to general await asyncio.sleep(2) await self.send_message( self.story_channel, @@ -241,3 +241,6 @@ async def cmd_start(self, message, content): message.channel, "Please wait until the current player finishes their game" ) + + + return bot From 255ad289329f5e2e2c6f5cb0d50aa704e91c95fb Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 17:34:38 -0500 Subject: [PATCH 06/23] Added balance and fixed score --- bots/story.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/bots/story.py b/bots/story.py index 89f351c..e3f7e76 100644 --- a/bots/story.py +++ b/bots/story.py @@ -117,6 +117,9 @@ def EnableStory(bot): @bot.add_command('!_stories') async def cmd_story(self, message, content): + """ + `!_stories` : Lists the available stories + """ games = [ f[:-3] for f in os.listdir('games') if f.endswith('.z5') ] @@ -160,6 +163,8 @@ async def state_router(self, message, content): '```'+self.player.readchunk()+'```' ) elif content == 'score': + self.player.write('score') + self.player.readchunk() await self.send_message( message.channel, 'Your score is %d' % self.player.score @@ -171,6 +176,7 @@ async def state_router(self, message, content): 'You have quit your game. Your score was %d' % self.player.score ) state['user'] = '~' + del state['transcript'] del self.player save_db(state, 'game.json') else: @@ -182,15 +188,20 @@ async def state_router(self, message, content): '```'+self.player.readchunk()+'```' ) else: - await self.delete_message(message) await self.send_message( message.author, "Please refrain from posting messages in the story channel" " while someone else is playing" ) + await asyncio.sleep(0.5) + await self.delete_message(message) @bot.add_command('!_start') async def cmd_start(self, message, content): + """ + `!_start ` : Starts an interactive text adventure + Example: `!_start zork1` + """ state = load_db('game.json', {'user':'~'}) if state['user'] == '~': games = { @@ -244,3 +255,38 @@ async def cmd_start(self, message, content): return bot + + def xp_for(level): + if level <= 1: + return 10 + else: + return (2*xp_for(level-1)-xp_for(level-2))+5 + + @bot.add_command('!_balance') + async def cmd_balance(self, message, content): + """ + `!_balance` : Displays your current token balance + """ + players = load_db('players.json') + if message.author.id not in players: + players[message.author.id] = { + 'level':1, + 'xp':0, + 'balance':10 + } + player = players[message.author.id] + await self.send_message( + message.channel, + "You are currently level %d and have a balance of %d tokens\n" + "You have %d xp to go to reach the next level" % ( + player['level'], + player['balance'], + xp_for(player['level']+1)-player['xp'] + ) + ) + + @bot.add_command('!_bid') + async def cmd_bid(self, message, content): + """ + `!_bid ` : Place a bid to play the next game + """ From e4c4b28b6ca85affa11ee8937bed0460d6e2f46d Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 10 Jan 2018 17:34:59 -0500 Subject: [PATCH 07/23] Syntax error --- bots/story.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bots/story.py b/bots/story.py index e3f7e76..2a88a58 100644 --- a/bots/story.py +++ b/bots/story.py @@ -290,3 +290,4 @@ async def cmd_bid(self, message, content): """ `!_bid ` : Place a bid to play the next game """ + pass From c1fd516a77c92456dbb8e9b31e5ee264e8283f78 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Sun, 28 Jan 2018 12:51:01 -0500 Subject: [PATCH 08/23] Updated story to use channel references --- bots/story.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bots/story.py b/bots/story.py index 2a88a58..4bc8e1f 100644 --- a/bots/story.py +++ b/bots/story.py @@ -115,6 +115,8 @@ def EnableStory(bot): if not isinstance(bot, CoreBot): raise TypeError("This function must take a CoreBot") + bot.reserve_channel('story') + @bot.add_command('!_stories') async def cmd_story(self, message, content): """ @@ -133,7 +135,7 @@ async def cmd_story(self, message, content): def checker(self, message): state = load_db('game.json', {'user':'~'}) - return message.channel.id == self.story_channel.id and state['user'] != '~' and not message.content.startswith('!') + return message.channel.id == self.fetch_channel('story').id and state['user'] != '~' and not message.content.startswith('!') @bot.add_special(checker) async def state_router(self, message, content): @@ -229,7 +231,7 @@ async def cmd_start(self, message, content): ' ones that I handle personally' ) await self.send_message( - self.story_channel, + self.fetch_channel('story'), '%s is now playing %s\n' 'The game will begin shortly' % ( message.author.mention, @@ -239,7 +241,7 @@ async def cmd_start(self, message, content): # Post to general await asyncio.sleep(2) await self.send_message( - self.story_channel, + self.fetch_channel('story'), '```'+self.player.readchunk()+'```' ) else: From c8f1647b9e48aa6b7549a112a68e7f166dc37a1d Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Thu, 1 Feb 2018 13:29:21 -0500 Subject: [PATCH 09/23] XP now runs from level 2 --- bots/story.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots/story.py b/bots/story.py index 4bc8e1f..706603e 100644 --- a/bots/story.py +++ b/bots/story.py @@ -259,7 +259,7 @@ async def cmd_start(self, message, content): return bot def xp_for(level): - if level <= 1: + if level <= 2: return 10 else: return (2*xp_for(level-1)-xp_for(level-2))+5 From 67c8815cf58c0f7ec794766b1fd10d47297e6561 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Thu, 8 Feb 2018 13:48:09 -0500 Subject: [PATCH 10/23] Added XP system There's also a bit of the token system in there --- bots/core.py | 1 + bots/story.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-- main.py | 5 +++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/bots/core.py b/bots/core.py index bb09929..b4b1d57 100644 --- a/bots/core.py +++ b/bots/core.py @@ -28,6 +28,7 @@ async def on_cmd(self, cmd, message, content): if self.check_permissions_chain(content[0][1:], message.author)[0]: print("Command in channel", message.channel, "from", message.author, ":", content) await func(self, message, content) + self.dispatch('command', content[0], message.author) else: print("Denied", message.author, "using command", content[0], "in", message.channel) await self.send_message( diff --git a/bots/story.py b/bots/story.py index 706603e..cfc2612 100644 --- a/bots/story.py +++ b/bots/story.py @@ -172,12 +172,19 @@ async def state_router(self, message, content): 'Your score is %d' % self.player.score ) elif content == 'quit': + self.player.write('score') + self.player.readchunk() self.player.quit() await self.send_message( message.channel, 'You have quit your game. Your score was %d' % self.player.score ) state['user'] = '~' + self.dispatch( + 'grant_xp', + message.author, + self.player.score * 10 #maybe normalize this since each game scores differently + ) del state['transcript'] del self.player save_db(state, 'game.json') @@ -222,8 +229,8 @@ async def cmd_start(self, message, content): message.author, 'Here are the controls for the story-mode system:\n' 'Any message you type in the story channel will be interpreted' - ' as input to the game *unless* your message starts with `!`' - ' (discord commands) or `$` (story commands)\n' + ' as input to the game **unless** your message starts with `!`' + ' (my commands)\n' '`$` : Simply type `$` to enter a blank line to the game\n' '`quit` : Quits the game in progress\n' '`score` : View your score\n' @@ -264,6 +271,30 @@ def xp_for(level): else: return (2*xp_for(level-1)-xp_for(level-2))+5 + @bot.subscribe('grant_xp') + async def grant_xp(self, evt, user, xp): + players = load_db('players.json') + if user.id not in players: + players[user.id] = { + 'level':1, + 'xp':0, + 'balance':10 + } + player = players[user.id] + current_level = player['level'] + while player['xp'] >= xp_for(player['level']+1): + player['xp'] -= xp_for(player['level']+1) + player['level'] += 1 + if player['level'] > current_level: + await self.send_message( + user, + "Congratulations on reaching level %d! Your weekly token payout" + " and maximum token balance have both been increased. To check" + " your balance, type `!balance`" % player['level'] + ) + players[user.id] = player + self.save_db(players, 'players.json') + @bot.add_command('!_balance') async def cmd_balance(self, message, content): """ @@ -293,3 +324,55 @@ async def cmd_bid(self, message, content): `!_bid ` : Place a bid to play the next game """ pass + + @bot.subscribe('command') + async def record_command(self, evt, command, user): + week = load_db('weekly.json') + if user.id not in week: + week[user.id] = {} + if 'commands' not in week[user.id]: + week[user.id]['commands'] = [command] + self.dispatch( + 'grant_xp', + user, + 5 + ) + elif command not in week[user.id]['commands']: + week[user.id]['commands'].append(command) + self.dispatch( + 'grant_xp', + user, + 5 + ) + save_db(week, 'weekly.json') + + @bot.add_task(604800) # 1 week + async def reset_week(self): + #{uid: {}} + week = load_db('weekly.json') + players = load_db('players.json') + xp = [] + for uid in weekly: + if 'active' in weekly[uid]: + xp.append([user, 5]) + user = self.fetch_channel('story').server.get_member(uid) #icky! + payout = players[user.id]['level'] + if players[user.id]['balance'] < 20*players[user.id]['level']: + payout *= 2 + players[uid]['balance'] += payout + await self.send_message( + self.fetch_channel('story').server.get_member(uid), #icky! + "Your allowance was %d tokens this week. Your balance is now %d " + "tokens" % ( + payout, + players[uid]['balance'] + ) + ) + save_db(players, 'players.json') + save_db({}, 'weekly.json') + for user, payout in xp: + self.dispatch( + 'grant_xp', + user, + payout + ) diff --git a/main.py b/main.py index 6351caf..6a06358 100644 --- a/main.py +++ b/main.py @@ -120,6 +120,11 @@ async def react(self, message, content): message, b'\xf0\x9f\x91\x8d'.decode() if random.random() < 0.8 else b'\xf0\x9f\x8d\x86'.decode() # :thumbsup: and sometimes :eggplant: ) + self.dispatch( + 'grant_xp', + message.author, + 2 + ) beymax.EnableAll( #enable all sub-bots EnableUtils, From 745f1720a589fe8b321eed03b60d66ac02dbe0e4 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Tue, 20 Feb 2018 14:27:35 -0500 Subject: [PATCH 11/23] Working on story XP --- bots/core.py | 25 ++++++++++++++-------- bots/story.py | 57 ++++++++++++++++++++++++++++++++++++++++++--------- main.py | 5 ++++- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/bots/core.py b/bots/core.py index b4b1d57..259f399 100644 --- a/bots/core.py +++ b/bots/core.py @@ -119,21 +119,27 @@ def EnableAll(self, *bots): #convenience function to enable a bunch of subbots a def dispatch(self, event, *args, manual=False, **kwargs): self.nt += 1 + output = [] if not manual: if 'before:'+str(event) in self.event_listeners: - self.dispatch_event('before:'+str(event), *args, **kwargs) + output += self.dispatch_event('before:'+str(event), *args, **kwargs) super().dispatch(event, *args, **kwargs) if str(event) in self.event_listeners: - self.dispatch_event(str(event), *args, **kwargs) + output += self.dispatch_event(str(event), *args, **kwargs) if 'after:'+str(event) in self.event_listeners: - self.dispatch_event('after:'+str(event), *args, **kwargs) + output += self.dispatch_event('after:'+str(event), *args, **kwargs) else: if str(event) in self.event_listeners: - self.dispatch_event(str(event), *args, **kwargs) + output += self.dispatch_event(str(event), *args, **kwargs) + return output def dispatch_event(self, event, *args, **kwargs): - for listener in self.event_listeners[event]: + return [ create_task(listener(self, event, *args, **kwargs), loop=self.loop) + for listener in self.event_listeners[event] + ] + + def config_get(self, *keys): obj = self.configuration @@ -291,10 +297,13 @@ async def on_ready(self): self.permissions['roles'][role]['role'] ) - async def close(self): + async def shutdown(self): save_db(self.users, 'users.json') - self.dispatch('cleanup') - await super().close() + tasks = self.dispatch('cleanup') + if len(tasks): + print("Waiting for ", len(tasks), "cleanup tasks to complete") + await asyncio.wait(tasks) + await self.close() async def send_message(self, destination, content, *, delim='\n', **kwargs): #built in chunking diff --git a/bots/story.py b/bots/story.py index cfc2612..286ffbf 100644 --- a/bots/story.py +++ b/bots/story.py @@ -116,6 +116,7 @@ def EnableStory(bot): raise TypeError("This function must take a CoreBot") bot.reserve_channel('story') + bot._pending_activity = set() @bot.add_command('!_stories') async def cmd_story(self, message, content): @@ -180,6 +181,7 @@ async def state_router(self, message, content): 'You have quit your game. Your score was %d' % self.player.score ) state['user'] = '~' + print("Granting xp for score payout") self.dispatch( 'grant_xp', message.author, @@ -263,8 +265,6 @@ async def cmd_start(self, message, content): ) - return bot - def xp_for(level): if level <= 2: return 10 @@ -272,7 +272,12 @@ def xp_for(level): return (2*xp_for(level-1)-xp_for(level-2))+5 @bot.subscribe('grant_xp') - async def grant_xp(self, evt, user, xp): + async def grant_some_xp(self, evt, user, xp): + print( + ": %d xp has been granted to %s" % ( + xp, str(user) + ) + ) players = load_db('players.json') if user.id not in players: players[user.id] = { @@ -281,6 +286,7 @@ async def grant_xp(self, evt, user, xp): 'balance':10 } player = players[user.id] + player['xp'] += xp current_level = player['level'] while player['xp'] >= xp_for(player['level']+1): player['xp'] -= xp_for(player['level']+1) @@ -293,12 +299,12 @@ async def grant_xp(self, evt, user, xp): " your balance, type `!balance`" % player['level'] ) players[user.id] = player - self.save_db(players, 'players.json') + save_db(players, 'players.json') - @bot.add_command('!_balance') + @bot.add_command('!balance') async def cmd_balance(self, message, content): """ - `!_balance` : Displays your current token balance + `!balance` : Displays your current token balance """ players = load_db('players.json') if message.author.id not in players: @@ -309,7 +315,7 @@ async def cmd_balance(self, message, content): } player = players[message.author.id] await self.send_message( - message.channel, + message.author, "You are currently level %d and have a balance of %d tokens\n" "You have %d xp to go to reach the next level" % ( player['level'], @@ -330,8 +336,10 @@ async def record_command(self, evt, command, user): week = load_db('weekly.json') if user.id not in week: week[user.id] = {} + print(week) if 'commands' not in week[user.id]: week[user.id]['commands'] = [command] + print("granting xp for first command", command) self.dispatch( 'grant_xp', user, @@ -339,6 +347,7 @@ async def record_command(self, evt, command, user): ) elif command not in week[user.id]['commands']: week[user.id]['commands'].append(command) + print("granting xp for new command", command) self.dispatch( 'grant_xp', user, @@ -346,16 +355,40 @@ async def record_command(self, evt, command, user): ) save_db(week, 'weekly.json') + @bot.subscribe('after:message') + async def record_activity(self, evt, message): + self._pending_activity.add(message.author.id) + + @bot.subscribe('cleanup') + async def save_activity(self, evt): + week = load_db('weekly') + print(week, self._pending_activity) + for uid in self._pending_activity: + if uid not in week: + week[uid]={'active':'yes'} + else: + week[uid]['active']='yes' + self._pending_activity = set() + print(week) + save_db(week, 'weekly.json') + @bot.add_task(604800) # 1 week async def reset_week(self): #{uid: {}} week = load_db('weekly.json') players = load_db('players.json') + print("Resetting the week") xp = [] - for uid in weekly: - if 'active' in weekly[uid]: - xp.append([user, 5]) + for uid in week: user = self.fetch_channel('story').server.get_member(uid) #icky! + if 'active' in week[uid] or uid in self._pending_activity: + xp.append([user, 5]) + if uid not in players: + players[uid] = { + 'level':1, + 'xp':0, + 'balance':10 + } payout = players[user.id]['level'] if players[user.id]['balance'] < 20*players[user.id]['level']: payout *= 2 @@ -368,11 +401,15 @@ async def reset_week(self): players[uid]['balance'] ) ) + self._pending_activity = set() save_db(players, 'players.json') save_db({}, 'weekly.json') for user, payout in xp: + print("granting xp for activity payout") self.dispatch( 'grant_xp', user, payout ) + + return bot diff --git a/main.py b/main.py index 6a06358..065858b 100644 --- a/main.py +++ b/main.py @@ -70,6 +70,8 @@ async def ready_up(self, event): print(channel.name, channel.type) print("Ready to serve!") self.dispatch('task:update_status') # manually set status at startup + from bots.utils import load_db + print(load_db('weekly.json')) @beymax.subscribe('member_join') async def greet_member(self, event, member): #greet new members @@ -84,7 +86,7 @@ async def cmd_shutdown(self, message, content): """ `!satisfied` : Shuts down beymax """ - await self.close() + await self.shutdown() @beymax.add_command('!_greet') async def cmd_greet(self, message, content): @@ -120,6 +122,7 @@ async def react(self, message, content): message, b'\xf0\x9f\x91\x8d'.decode() if random.random() < 0.8 else b'\xf0\x9f\x8d\x86'.decode() # :thumbsup: and sometimes :eggplant: ) + print("granting reaction xp") self.dispatch( 'grant_xp', message.author, From e29f4e2810d4d99f65babc0673450f654e3f2186 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 12:59:16 -0500 Subject: [PATCH 12/23] Refactored database to be async safe --- bots/birthday.py | 50 +++--- bots/bug.py | 428 +++++++++++++++++++++++----------------------- bots/cash.py | 434 +++++++++++++++++++++++------------------------ bots/core.py | 2 +- bots/help.py | 99 +++++------ bots/ow.py | 190 ++++++++++----------- bots/party.py | 380 ++++++++++++++++++++--------------------- bots/story.py | 424 ++++++++++++++++++++++----------------------- bots/utils.py | 89 ++++++++++ main.py | 7 +- 10 files changed, 1099 insertions(+), 1004 deletions(-) diff --git a/bots/birthday.py b/bots/birthday.py index a3c4d4f..d700f8a 100644 --- a/bots/birthday.py +++ b/bots/birthday.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import load_db, save_db +from .utils import Database import asyncio import re import datetime @@ -22,33 +22,33 @@ async def cmd_birthday(self, message, content): ) else: result = re.match(r'(\d{1,2})/(\d{1,2})/(\d{4})', content[1]) - birthdays = load_db('birthdays.json') - birthdays[message.author.id] = { - 'month': int(result.group(1)), - 'day': int(result.group(2)), - 'year': int(result.group(3)) - } - await self.send_message( - message.channel, - "Okay, I'll remember that" - ) - save_db(birthdays, 'birthdays.json') + async with Database('birthdays.json') as birthdays: + birthdays[message.author.id] = { + 'month': int(result.group(1)), + 'day': int(result.group(2)), + 'year': int(result.group(3)) + } + await self.send_message( + message.channel, + "Okay, I'll remember that" + ) + birthdays.save() @bot.add_task(43200) #12 hours async def check_birthday(self): - birthdays = load_db('birthdays.json') - today = datetime.date.today() - for uid, data in birthdays.items(): - month = data['month'] - day = data['day'] - if today.day == day and today.month == month: - await self.send_message( - self.fetch_channel('general'), - "@everyone congratulate %s, for today is their birthday!" - " They are %d!" % ( - self.users[uid]['mention'] if uid in self.users else "someone", - today.year - data['year'] + async with Database('birthdays.json') as birthdays: + today = datetime.date.today() + for uid, data in birthdays.items(): + month = data['month'] + day = data['day'] + if today.day == day and today.month == month: + await self.send_message( + self.fetch_channel('general'), + "@everyone congratulate %s, for today is their birthday!" + " They are %d!" % ( + self.users[uid]['mention'] if uid in self.users else "someone", + today.year - data['year'] + ) ) - ) return bot diff --git a/bots/bug.py b/bots/bug.py index 1e734b3..e7ed056 100644 --- a/bots/bug.py +++ b/bots/bug.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import load_db, save_db, getname +from .utils import ListDatabase, getname import asyncio def EnableBugs(bot): @@ -14,35 +14,35 @@ async def cmd_bug(self, message, content): `!bug [feedback or bug report]` : Opens a new ticket with your feedback. Example: `!bug Beymax didn't understand me in a help session` """ - bugs = load_db('bugs.json', []) - bugs.append({ - 'users': [message.author.id], - 'status': 'Pending', #pending->investigating->solution in progress->testing solution->closed - 'content': ' '.join(content[1:]), - 'comments':[], - 'label': ' '.join(content[1:]) - }) - role_mention = '' - role_target = self.config_get('bug_role') - if role_target is not None: - for role in self.fetch_channel('bugs').server.roles: - # Not that this will make literally 0 sense in a multi-server environment - # primaryServerMasterRace - if role.id == role_target or role.name == role_target: - role_mention = role.mention - if role_mention == '': - raise NameError("No role '%s'" % role_target) - await self.send_message( - self.fetch_channel('bugs'), - 'New issue reported: %s\n' #@Developer - '[%d] [Pending] %s : %s' % ( - role_mention, - len(bugs)-1, - message.author.mention, - bugs[-1]['content'] + async with ListDatabase('bugs.json') as bugs: + bugs.append({ + 'users': [message.author.id], + 'status': 'Pending', #pending->investigating->solution in progress->testing solution->closed + 'content': ' '.join(content[1:]), + 'comments':[], + 'label': ' '.join(content[1:]) + }) + role_mention = '' + role_target = self.config_get('bug_role') + if role_target is not None: + for role in self.fetch_channel('bugs').server.roles: + # Not that this will make literally 0 sense in a multi-server environment + # primaryServerMasterRace + if role.id == role_target or role.name == role_target: + role_mention = role.mention + if role_mention == '': + raise NameError("No role '%s'" % role_target) + await self.send_message( + self.fetch_channel('bugs'), + 'New issue reported: %s\n' #@Developer + '[%d] [Pending] %s : %s' % ( + role_mention, + len(bugs)-1, + message.author.mention, + bugs[-1]['content'] + ) ) - ) - save_db(bugs, 'bugs.json') + bugs.save() @bot.add_command('!thread', '!bug:thread') async def cmd_thread(self, message, content): @@ -50,36 +50,36 @@ async def cmd_thread(self, message, content): `!thread ` : Displays the full comment thread for a bug. Example: `!thread 2` """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): - await self.send_message( - message.channel, - "No bug with that ID" - ) - else: - body = '[%d] [%s] %s : %s\n' % ( - bugid, - bugs[bugid]['status'], - ' '.join( - self.users[user]['name'] for user in - bugs[bugid]['users'] - ), - bugs[bugid]['label'], - ) - body += 'Issue: %s\n' % bugs[bugid]['content'] - for comment in bugs[bugid]['comments']: - body += 'Comment by %s\n' % comment + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): + await self.send_message( + message.channel, + "No bug with that ID" + ) + else: + body = '[%d] [%s] %s : %s\n' % ( + bugid, + bugs[bugid]['status'], + ' '.join( + self.users[user]['name'] for user in + bugs[bugid]['users'] + ), + bugs[bugid]['label'], + ) + body += 'Issue: %s\n' % bugs[bugid]['content'] + for comment in bugs[bugid]['comments']: + body += 'Comment by %s\n' % comment + await self.send_message( + message.channel, + body + ) + except: await self.send_message( message.channel, - body + "Unable to parse the bug ID from the message" ) - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) @bot.add_command('!comment', '!bug:comment') async def cmd_comment(self, message, content): @@ -87,44 +87,44 @@ async def cmd_comment(self, message, content): `!comment [Your comments]` : Adds your comments to the bug's thread. Example: `!comment 2 The help system is working great!` """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): - await self.send_message( - message.channel, - "No bug with that ID" - ) - else: - comment = ' '.join(content[2:]) - bugs[bugid]['comments'].append( - '%s : %s' % ( - getname(message.author), - comment + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): + await self.send_message( + message.channel, + "No bug with that ID" ) - ) - await self.send_message( - self.fetch_channel('bugs'), - 'New comment on issue:\n' - '[%d] [%s] %s : %s\n' - 'Comment: [%s] : %s' % ( - bugid, - bugs[bugid]['status'], - ' '.join( - self.users[user]['mention'] for user in - bugs[bugid]['users'] - ), - bugs[bugid]['label'], - message.author.mention, - comment + else: + comment = ' '.join(content[2:]) + bugs[bugid]['comments'].append( + '%s : %s' % ( + getname(message.author), + comment + ) + ) + await self.send_message( + self.fetch_channel('bugs'), + 'New comment on issue:\n' + '[%d] [%s] %s : %s\n' + 'Comment: [%s] : %s' % ( + bugid, + bugs[bugid]['status'], + ' '.join( + self.users[user]['mention'] for user in + bugs[bugid]['users'] + ), + bugs[bugid]['label'], + message.author.mention, + comment + ) ) + bugs.save() + except: + await self.send_message( + message.channel, + "Unable to parse the bug ID from the message" ) - save_db(bugs, 'bugs.json') - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) @bot.add_command('!bug:status') async def cmd_bug_status(self, message, content): @@ -132,35 +132,35 @@ async def cmd_bug_status(self, message, content): `!bug:status ` : Sets the status for the bug. Example: `!bug:status 2 In Progress` """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): + await self.send_message( + message.channel, + "No bug with that ID" + ) + else: + bugs[bugid]['status'] = ' '.join(content[2:]) + await self.send_message( + self.fetch_channel('bugs'), + 'Issue status changed:\n' + '[%d] [%s] %s : %s' % ( + bugid, + bugs[bugid]['status'], + ' '.join( + self.users[user]['mention'] for user in + bugs[bugid]['users'] + ), + bugs[bugid]['label'], + ) + ) + bugs.save() + except: await self.send_message( message.channel, - "No bug with that ID" - ) - else: - bugs[bugid]['status'] = ' '.join(content[2:]) - await self.send_message( - self.fetch_channel('bugs'), - 'Issue status changed:\n' - '[%d] [%s] %s : %s' % ( - bugid, - bugs[bugid]['status'], - ' '.join( - self.users[user]['mention'] for user in - bugs[bugid]['users'] - ), - bugs[bugid]['label'], - ) + "Unable to parse the bug ID from the message" ) - save_db(bugs, 'bugs.json') - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) @bot.add_command('!bug:label') async def cmd_bug_label(self, message, content): @@ -168,38 +168,38 @@ async def cmd_bug_label(self, message, content): `!bug:label ` : Sets the label for a bug report. Example: `!bug:label 2 Beymax's help system` """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): + await self.send_message( + message.channel, + "No bug with that ID" + ) + else: + label = ' '.join(content[2:]) + await self.send_message( + self.fetch_channel('bugs'), + 'Issue label changed:\n' + '[%d] [%s] %s : %s\n' + 'New label: %s' % ( + bugid, + bugs[bugid]['status'], + ' '.join( + self.users[user]['mention'] for user in + bugs[bugid]['users'] + ), + bugs[bugid]['label'], + label + ) + ) + bugs[bugid]['label'] = label + bugs.save() + except: await self.send_message( message.channel, - "No bug with that ID" - ) - else: - label = ' '.join(content[2:]) - await self.send_message( - self.fetch_channel('bugs'), - 'Issue label changed:\n' - '[%d] [%s] %s : %s\n' - 'New label: %s' % ( - bugid, - bugs[bugid]['status'], - ' '.join( - self.users[user]['mention'] for user in - bugs[bugid]['users'] - ), - bugs[bugid]['label'], - label - ) + "Unable to parse the bug ID from the message" ) - bugs[bugid]['label'] = label - save_db(bugs, 'bugs.json') - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) @bot.add_command('!bug:user') async def cmd_bug_user(self, message, content): @@ -207,46 +207,46 @@ async def cmd_bug_user(self, message, content): `!bug:user ` : Subscribes a user to a bug report. Example: `!bug:user 2 310283932341895169` (that's my user ID) """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): - await self.send_message( - message.channel, - "No bug with that ID" - ) - else: - try: - user = await self.get_user_info(content[2]) - bugs[bugid]['users'].append(user.id) - await self.send_message( - user, - "You have been added to the following issue by %s:\n" - '[%d] [%s] : %s\n' - 'If you would like to unsubscribe from this issue, ' - 'type `!bug:unsubscribe %d`'% ( - str(message.author), - bugid, - bugs[bugid]['status'], - bugs[bugid]['label'], - bugid - ) - ) - await self.send_message( - message.channel, - "Added user to issue" - ) - save_db(bugs, 'bugs.json') - except: + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): await self.send_message( message.channel, - "No user with that ID" + "No bug with that ID" ) - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) + else: + try: + user = await self.get_user_info(content[2]) + bugs[bugid]['users'].append(user.id) + await self.send_message( + user, + "You have been added to the following issue by %s:\n" + '[%d] [%s] : %s\n' + 'If you would like to unsubscribe from this issue, ' + 'type `!bug:unsubscribe %d`'% ( + str(message.author), + bugid, + bugs[bugid]['status'], + bugs[bugid]['label'], + bugid + ) + ) + await self.send_message( + message.channel, + "Added user to issue" + ) + bugs.save() + except: + await self.send_message( + message.channel, + "No user with that ID" + ) + except: + await self.send_message( + message.channel, + "Unable to parse the bug ID from the message" + ) @bot.add_command('!bug:unsubscribe') async def cmd_bug_unsubscribe(self, message, content): @@ -254,41 +254,41 @@ async def cmd_bug_unsubscribe(self, message, content): `!bug:unsubscribe ` : Unsubscribes yourself from a bug report. Example: `!bug:unsubscribe 2` """ - bugs = load_db('bugs.json', []) - try: - bugid = int(content[1]) - if bugid >= len(bugs): - await self.send_message( - message.channel, - "No bug with that ID" - ) - else: - if bugs[bugid]['users'][0] == message.author.id: - await self.send_message( - message.channel, - "As the creator of this issue, you cannot unsubscribe" - ) - elif message.author.id not in bugs[bugid]['users']: + async with ListDatabase('bugs.json') as bugs: + try: + bugid = int(content[1]) + if bugid >= len(bugs): await self.send_message( message.channel, - "You are not subscribed to this issue" + "No bug with that ID" ) else: - bugs[bugid]['users'].remove(message.author.id) - await self.send_message( - message.channel, - "You have been unsubscribed from this issue:\n" - '[%d] [%s] : %s' % ( - bugid, - bugs[bugid]['status'], - bugs[bugid]['label'] + if bugs[bugid]['users'][0] == message.author.id: + await self.send_message( + message.channel, + "As the creator of this issue, you cannot unsubscribe" ) - ) - save_db(bugs, 'bugs.json') - except: - await self.send_message( - message.channel, - "Unable to parse the bug ID from the message" - ) + elif message.author.id not in bugs[bugid]['users']: + await self.send_message( + message.channel, + "You are not subscribed to this issue" + ) + else: + bugs[bugid]['users'].remove(message.author.id) + await self.send_message( + message.channel, + "You have been unsubscribed from this issue:\n" + '[%d] [%s] : %s' % ( + bugid, + bugs[bugid]['status'], + bugs[bugid]['label'] + ) + ) + bugs.save() + except: + await self.send_message( + message.channel, + "Unable to parse the bug ID from the message" + ) return bot diff --git a/bots/cash.py b/bots/cash.py index 578ab12..4e5c693 100644 --- a/bots/cash.py +++ b/bots/cash.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import load_db, save_db +from .utils import Database import asyncio import time import datetime @@ -31,51 +31,51 @@ async def cmd_payment(self, message, content): ) else: # cash : {project : {goal, current, title, contributions, notified, end, account}} - cash = load_db('cash.json') - if content[1] not in cash: - await self.send_message( - message.channel, - 'Project %s not found. Current projects: %s' % ( - content[1], - ' '.join('"%s"' % k for k in cash) + async with Database('cash.json') as cash: + if content[1] not in cash: + await self.send_message( + message.channel, + 'Project %s not found. Current projects: %s' % ( + content[1], + ' '.join('"%s"' % k for k in cash) + ) ) - ) - else: - project = content[1] - uid = content[2] - try: - amount = float(content[3][1:]) - except ValueError: - amount = int(contents[3][1:]) - cash[project]['current'] += amount - cash[project]['contributions'].append( - { - 'time':time.time(), - 'user': uid, - 'amount': amount - } - ) - await self.send_message( - self.fetch_channel('general'), - '@everyone %s has generously donated $%0.2f towards %s, which puts us' - ' at %.0f%% of the $%d goal.\n' - 'There is $%0.2f left to raise by %d/%d/%d\n' - 'If you would like to donate, ' - 'venmo `%s` and mention `%s` in the payment' % ( - self.users[uid]['mention'] if uid in self.users else 'someone', - amount, - cash[project]['title'], - 100*(cash[project]['current']/cash[project]['goal']), - cash[project]['goal'], - cash[project]['goal']-cash[project]['current'], - cash[project]['end']['month'], - cash[project]['end']['day'], - cash[project]['end']['year'], - cash[project]['account'], - project + else: + project = content[1] + uid = content[2] + try: + amount = float(content[3][1:]) + except ValueError: + amount = int(contents[3][1:]) + cash[project]['current'] += amount + cash[project]['contributions'].append( + { + 'time':time.time(), + 'user': uid, + 'amount': amount + } ) - ) - save_db(cash, 'cash.json') + await self.send_message( + self.fetch_channel('general'), + '@everyone %s has generously donated $%0.2f towards %s, which puts us' + ' at %.0f%% of the $%d goal.\n' + 'There is $%0.2f left to raise by %d/%d/%d\n' + 'If you would like to donate, ' + 'venmo `%s` and mention `%s` in the payment' % ( + self.users[uid]['mention'] if uid in self.users else 'someone', + amount, + cash[project]['title'], + 100*(cash[project]['current']/cash[project]['goal']), + cash[project]['goal'], + cash[project]['goal']-cash[project]['current'], + cash[project]['end']['month'], + cash[project]['end']['day'], + cash[project]['end']['year'], + cash[project]['account'], + project + ) + ) + cash.save() @bot.add_command('!_project') async def cmd_project(self, message, content): @@ -93,95 +93,95 @@ async def cmd_project(self, message, content): "MM/DD/YYYY | $goal | venmo account`" ) else: - cash = load_db('cash.json') - print(args) - full = args[0].strip() - short = args[1].strip() - end = args[2].strip() - goal = args[3].strip() - account = args[4].strip() - if len(short.split())>1: - await self.send_message( - message.channel, - "Syntax is: `!_project full name | short name | end date " - "MM/DD/YYYY | $goal | venmo account`. " - "There can be no spaces in the short name" - ) - elif not date_pattern.match(end): - await self.send_message( - message.channel, - "Syntax is: `!_project full name | short name | end date " - "MM/DD/YYYY | $goal | venmo account`. " - "End date must be in MM/DD/YYYY format" - ) - elif not goal.startswith('$'): - await self.send_message( - message.channel, - "Syntax is: `!_project full name | short name | end date " - "MM/DD/YYYY | $goal | venmo account`. " - "Goal must be an integer and start with '$'" - ) - elif short in cash: - await self.send_message( - message.channel, - "A project with that short name already exists. (Short name" - " must be unique)" - ) - else: - try: - goal = int(goal[1:]) - end = date_pattern.match(end) - # cash : {project : {goal, current, title, contributions, notified, end, account}} - cash[short] = { - 'goal': goal, - 'current': 0, - 'title': full, - 'contributions': [], - 'notified': time.time(), - 'end': { - 'year': int(end.group(3)), - 'month': int(end.group(1)), - 'day': int(end.group(2)) - }, - 'account': account - } + async with Database('cash.json') as cash: + print(args) + full = args[0].strip() + short = args[1].strip() + end = args[2].strip() + goal = args[3].strip() + account = args[4].strip() + if len(short.split())>1: await self.send_message( - self.fetch_channel('general'), - '%s has started a new funding project:\n' - 'Raise $%d by %s for %s\n' - 'If you would like to donate, venmo `%s` and mention `%s`' - ' in the payment\n' - 'Remember, all projects are pay-what-you-want' % ( - message.author.mention, - goal, - args[2], - full, - account, - short - ) + message.channel, + "Syntax is: `!_project full name | short name | end date " + "MM/DD/YYYY | $goal | venmo account`. " + "There can be no spaces in the short name" ) + elif not date_pattern.match(end): await self.send_message( - message.author, - "You have created the funding project `%s`. Currently, " - "you must manually notify me when you get paid. The command" - " for this is `!_payment`.\nFor example, if I paid you $10" - ", you would use `!_payment %s %s $10` \n(that number is my user id)." - " To get user IDs, you must be in development mode, then" - " right click on a user and select 'Copy ID'.\n" - "Use `0` as the user ID to record an anonymous payment" % ( - short, - short, - self.user.id, - ) + message.channel, + "Syntax is: `!_project full name | short name | end date " + "MM/DD/YYYY | $goal | venmo account`. " + "End date must be in MM/DD/YYYY format" ) - save_db(cash, 'cash.json') - except: + elif not goal.startswith('$'): await self.send_message( message.channel, "Syntax is: `!_project full name | short name | end date " - "MM/DD/YY | $goal | venmo account`. " + "MM/DD/YYYY | $goal | venmo account`. " "Goal must be an integer and start with '$'" ) + elif short in cash: + await self.send_message( + message.channel, + "A project with that short name already exists. (Short name" + " must be unique)" + ) + else: + try: + goal = int(goal[1:]) + end = date_pattern.match(end) + # cash : {project : {goal, current, title, contributions, notified, end, account}} + cash[short] = { + 'goal': goal, + 'current': 0, + 'title': full, + 'contributions': [], + 'notified': time.time(), + 'end': { + 'year': int(end.group(3)), + 'month': int(end.group(1)), + 'day': int(end.group(2)) + }, + 'account': account + } + await self.send_message( + self.fetch_channel('general'), + '%s has started a new funding project:\n' + 'Raise $%d by %s for %s\n' + 'If you would like to donate, venmo `%s` and mention `%s`' + ' in the payment\n' + 'Remember, all projects are pay-what-you-want' % ( + message.author.mention, + goal, + args[2], + full, + account, + short + ) + ) + await self.send_message( + message.author, + "You have created the funding project `%s`. Currently, " + "you must manually notify me when you get paid. The command" + " for this is `!_payment`.\nFor example, if I paid you $10" + ", you would use `!_payment %s %s $10` \n(that number is my user id)." + " To get user IDs, you must be in development mode, then" + " right click on a user and select 'Copy ID'.\n" + "Use `0` as the user ID to record an anonymous payment" % ( + short, + short, + self.user.id, + ) + ) + cash.save() + except: + await self.send_message( + message.channel, + "Syntax is: `!_project full name | short name | end date " + "MM/DD/YY | $goal | venmo account`. " + "Goal must be an integer and start with '$'" + ) @bot.add_command('!_project:end') async def cmd_end_project(self, message, content): @@ -195,109 +195,109 @@ async def cmd_end_project(self, message, content): "Syntax is: `!_project:end short_name`" ) else: - cash = load_db('cash.json') - project = content[1] - if project not in cash: - await self.send_message( - message.channel, - "No funding project with that name" - ) - else: - await self.send_message( - self.fetch_channel('general'), - "The funding project for %s has ended at %.0f%% of its $%d goal" % ( - cash[project]['title'], - 100*(cash[project]['current']/cash[project]['goal']), - cash[project]['goal'] + async with Database('cash.json') as cash: + project = content[1] + if project not in cash: + await self.send_message( + message.channel, + "No funding project with that name" ) - + ( - '\nDonations:\n' + - '\n'.join( - '%s: $%d' % ( - self.users[contrib['user']]['mention'] if contrib['user'] in self.users else 'Anonymous', - contrib['amount'] - ) - for contrib in sorted( - cash[project]['contributions'], - key=lambda x:x['amount'], - reverse=True + else: + await self.send_message( + self.fetch_channel('general'), + "The funding project for %s has ended at %.0f%% of its $%d goal" % ( + cash[project]['title'], + 100*(cash[project]['current']/cash[project]['goal']), + cash[project]['goal'] + ) + + ( + '\nDonations:\n' + + '\n'.join( + '%s: $%d' % ( + self.users[contrib['user']]['mention'] if contrib['user'] in self.users else 'Anonymous', + contrib['amount'] + ) + for contrib in sorted( + cash[project]['contributions'], + key=lambda x:x['amount'], + reverse=True + ) ) ) + + ( + "\nNice work, and thanks to all the donors!" if + cash[project]['current']>=cash[project]['goal'] + else "" + ) ) - + ( - "\nNice work, and thanks to all the donors!" if - cash[project]['current']>=cash[project]['goal'] - else "" - ) - ) - old_cash = load_db('old_cash.json') - old_cash[project] = cash[project] - save_db(old_cash, 'old_cash.json') - del cash[project] - save_db(cash, 'cash.json') + async with Database('old_cash.json') as old_cash: + old_cash[project] = cash[project] + old_cash.save() + del cash[project] + cash.save() @bot.add_task(604800) # 1 week async def notify_projects(self): - cash = load_db('cash.json') - today = datetime.date.today() - for project in [k for k in cash]: - data = cash[project] - end = data['end'] - ended = end['year'] < today.year - ended |= end['year'] == today.year and end['month'] < today.month - ended |= end['year'] == today.year and end['month'] == today.month and end['day'] < today.day - if ended: - await self.send_message( - self.fetch_channel('general'), - "The funding project for %s has ended at %.0f%% of its $%d goal" % ( - cash[project]['title'], - 100*(cash[project]['current']/cash[project]['goal']), - cash[project]['goal'] - ) - + ( - '\nDonations:\n' + - '\n'.join( - '%s: $%d' % ( - self.users[contrib['user']]['mention'] if contrib['user'] in self.users else 'Anonymous', - contrib['amount'] - ) - for contrib in sorted( - cash[project]['contributions'], - key=lambda x:x['amount'], - reverse=True + async with Database('cash.json') as cash: + today = datetime.date.today() + for project in [k for k in cash]: + data = cash[project] + end = data['end'] + ended = end['year'] < today.year + ended |= end['year'] == today.year and end['month'] < today.month + ended |= end['year'] == today.year and end['month'] == today.month and end['day'] < today.day + if ended: + await self.send_message( + self.fetch_channel('general'), + "The funding project for %s has ended at %.0f%% of its $%d goal" % ( + cash[project]['title'], + 100*(cash[project]['current']/cash[project]['goal']), + cash[project]['goal'] + ) + + ( + '\nDonations:\n' + + '\n'.join( + '%s: $%d' % ( + self.users[contrib['user']]['mention'] if contrib['user'] in self.users else 'Anonymous', + contrib['amount'] + ) + for contrib in sorted( + cash[project]['contributions'], + key=lambda x:x['amount'], + reverse=True + ) ) ) + + ( + "\nNice work, and thanks to all the donors!" if + cash[project]['current']>=cash[project]['goal'] + else "" + ) ) - + ( - "\nNice work, and thanks to all the donors!" if - cash[project]['current']>=cash[project]['goal'] - else "" - ) - ) - old_cash = load_db('old_cash.json') - old_cash[project] = data - save_db(old_cash, 'old_cash.json') - del cash[project] - elif time.time() - data['notified'] > 2628001: #~1 month - await self.send_message( - self.fetch_channel('general'), - "Funds are still being collected for %s\n" - "Current progress: $%0.2f/$%d (%.0f%%)\n" - "Project ends: %d/%d/%d\n" - 'If you would like to donate, venmo `%s` and mention `%s`' - ' in the payment' % ( - data['title'], - data['current'], - data['goal'], - 100*(data['current']/data['goal']), - end['month'], - end['day'], - end['year'], - data['account'], - project + async with Database('old_cash.json') as old_cash: + old_cash[project] = cash[project] + old_cash.save() + del cash[project] + elif time.time() - data['notified'] > 2628001: #~1 month + await self.send_message( + self.fetch_channel('general'), + "Funds are still being collected for %s\n" + "Current progress: $%0.2f/$%d (%.0f%%)\n" + "Project ends: %d/%d/%d\n" + 'If you would like to donate, venmo `%s` and mention `%s`' + ' in the payment' % ( + data['title'], + data['current'], + data['goal'], + 100*(data['current']/data['goal']), + end['month'], + end['day'], + end['year'], + data['account'], + project + ) ) - ) - cash[project]['notified'] = time.time() - save_db(cash, 'cash.json') + cash[project]['notified'] = time.time() + cash.save() return bot diff --git a/bots/core.py b/bots/core.py index 259f399..4168c05 100644 --- a/bots/core.py +++ b/bots/core.py @@ -1,4 +1,4 @@ -from .utils import load_db, save_db, getname, validate_permissions +from .utils import Database, getname, validate_permissions import discord from discord.compat import create_task import asyncio diff --git a/bots/help.py b/bots/help.py index 1a69d98..ce3bf6f 100644 --- a/bots/help.py +++ b/bots/help.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import sanitize, load_db +from .utils import sanitize, Database import discord import asyncio import sys @@ -249,23 +249,24 @@ async def digest(self, message): cmd = sanitize(message, '`~!@#$%^&*()-_=+{[]}\\|,.<>/?;:\'"').lower() print("Digest content:", cmd) if self.stage == 'default': - choice = binwords( - cmd, - bots=['bots', 'apps', 'robots'], - octavia=['octavia', 'tenno', 'dj', 'music'], - beymax=['beymax', 'baymax', 'jroot', 'dev', 'helper', 'you', 'yourself'], - beymax_commands=[command[1:] for command in self.client.commands], - channels=['channels', 'groups', 'messages', 'channel'], - general=['general'], - jukebox=['jukebox'], - testing_grounds=['testing', 'grounds', 'testing_grounds'], - rpgs=['rpgs', 'rpg'], - afk=['afk'], - party=['party'] + [ - party['name'].split() for party in load_db('parties.json', []) - ], - help=['help'], - ) + async with ListDatabase('parties.json') as parties: + choice = binwords( + cmd, + bots=['bots', 'apps', 'robots'], + octavia=['octavia', 'tenno', 'dj', 'music'], + beymax=['beymax', 'baymax', 'jroot', 'dev', 'helper', 'you', 'yourself'], + beymax_commands=[command[1:] for command in self.client.commands], + channels=['channels', 'groups', 'messages', 'channel'], + general=['general'], + jukebox=['jukebox'], + testing_grounds=['testing', 'grounds', 'testing_grounds'], + rpgs=['rpgs', 'rpg'], + afk=['afk'], + party=['party'] + [ + party['name'].split() for party in parties + ], + help=['help'], + ) if choice is None: await self.client.send_message( self.user, @@ -326,25 +327,26 @@ async def digest(self, message): else: await self.stage_terminal() elif self.stage == 'terminal': - choice = binwords( - cmd, - bots=['bots', 'apps', 'robots'], - octavia=['octavia', 'tenno', 'dj', 'music'], - beymax=['beymax', 'baymax', 'jroot', 'dev', 'helper', 'you', 'yourself'], - beymax_commands=[command[1:] for command in self.client.commands], - channels=['channels', 'groups', 'messages', 'channel'], - general=['general'], - jukebox=['jukebox'], - testing_grounds=['testing', 'grounds', 'testing_grounds'], - rpgs=['rpgs', 'rpg'], - afk=['afk'], - party=['party'] + [ - party['name'].split() for party in load_db('parties.json', []) - ], - help=['help'], - yes=['yes', 'sure', 'ok', 'yep', 'please', 'okay', 'yeah'], - no=['no', 'nope', 'nah', 'thanks'] - ) + async with ListDatabase('parties.json') as parties: + choice = binwords( + cmd, + bots=['bots', 'apps', 'robots'], + octavia=['octavia', 'tenno', 'dj', 'music'], + beymax=['beymax', 'baymax', 'jroot', 'dev', 'helper', 'you', 'yourself'], + beymax_commands=[command[1:] for command in self.client.commands], + channels=['channels', 'groups', 'messages', 'channel'], + general=['general'], + jukebox=['jukebox'], + testing_grounds=['testing', 'grounds', 'testing_grounds'], + rpgs=['rpgs', 'rpg'], + afk=['afk'], + party=['party'] + [ + party['name'].split() for party in parties + ], + help=['help'], + yes=['yes', 'sure', 'ok', 'yep', 'please', 'okay', 'yeah'], + no=['no', 'nope', 'nah', 'thanks'] + ) if choice is None: await self.client.send_message( self.user, @@ -376,17 +378,18 @@ async def digest(self, message): "Okay. Glad to be of service" ) elif self.stage == 'channels': - choice = binwords( - cmd, - general=['general'], - jukebox=['jukebox'], - testing_grounds=['testing', 'grounds', 'testing_grounds'], - rpgs=['rpgs', 'rpg'], - afk=['afk'], - party=['party'] + [ - party['name'].split() for party in load_db('parties.json', []) - ] - ) + async with ListDatabase('parties.json') as parties: + choice = binwords( + cmd, + general=['general'], + jukebox=['jukebox'], + testing_grounds=['testing', 'grounds', 'testing_grounds'], + rpgs=['rpgs', 'rpg'], + afk=['afk'], + party=['party'] + [ + party['name'].split() for party in parties + ] + ) if choice is None: await self.client.send_message( self.user, diff --git a/bots/ow.py b/bots/ow.py index 43dc411..6b473d1 100644 --- a/bots/ow.py +++ b/bots/ow.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import load_db, save_db +from .utils import Database import os import requests from requests.exceptions import RequestException @@ -83,36 +83,36 @@ async def update_overwatch(self): """ if os.path.isfile('stats_interim.json'): return - state = load_db('stats.json') - for uid, data in state.items(): - tag = data['tag'] - rating = data['rating'] - old_tier = data['tier'] if 'tier' in data else 'Unranked' - try: - current, img, tier = get_mmr(tag) - state[uid]['rating'] = current - state[uid]['avatar'] = img - state[uid]['tier'] = tier - currentRank = rank(tier) - oldRank = rank(old_tier) - if currentRank > oldRank: - body = "Everyone put your hands together for " - body += self.users[uid]['mention'] if uid in self.users else tag - body += " who just reached " - body += tier - body += " in Overwatch!" - if 'avatar' in state[uid]: - body += '\n'+state[uid]['avatar'] - if currentRank >= 4: - # Ping the channel for anyone who reached platinum or above - body = body.replace('Everyone', '@everyone') - await self.send_message( - self.fetch_channel('general'), #for now - body - ) - except RequestException: - pass - save_db(state, 'stats.json') + async with Database('stats.json') as state: + for uid, data in state.items(): + tag = data['tag'] + rating = data['rating'] + old_tier = data['tier'] if 'tier' in data else 'Unranked' + try: + current, img, tier = get_mmr(tag) + state[uid]['rating'] = current + state[uid]['avatar'] = img + state[uid]['tier'] = tier + currentRank = rank(tier) + oldRank = rank(old_tier) + if currentRank > oldRank: + body = "Everyone put your hands together for " + body += self.users[uid]['mention'] if uid in self.users else tag + body += " who just reached " + body += tier + body += " in Overwatch!" + if 'avatar' in state[uid]: + body += '\n'+state[uid]['avatar'] + if currentRank >= 4: + # Ping the channel for anyone who reached platinum or above + body = body.replace('Everyone', '@everyone') + await self.send_message( + self.fetch_channel('general'), #for now + body + ) + except RequestException: + pass + state.save() @bot.add_command('!owupdate') async def cmd_update(self, message, content): @@ -134,15 +134,15 @@ async def cmd_ow(self, message, content): else: username = content[1].replace('#', '-') try: - state = load_db(path) - get_mmr(username) - state[message.author.id] = { - 'tag': username, - 'rating': 0, - 'avatar':'', - 'tier':'Unranked' - } - save_db(state, path) + async with Database(path) as state: + get_mmr(username) + state[message.author.id] = { + 'tag': username, + 'rating': 0, + 'avatar':'', + 'tier':'Unranked' + } + state.save() await self.send_message( message.channel, "Alright! I'll keep track of your stats" @@ -162,50 +162,50 @@ async def cmd_owreset(self, message, content): """ `!_owreset` : Triggers the overwatch end-of-season message and sets stats tracking to interim mode """ - state = load_db('stats.json') - if len(state): - for uid, data in state.items(): - tag = data['tag'] - rating = data['rating'] - old_tier = data['tier'] if 'tier' in data else 'Unranked' - try: - current, img, tier = get_mmr(tag) - state[uid]['rating'] = current - state[uid]['avatar'] = img - state[uid]['tier'] = tier - except RequestException: - pass - ranked = [(data['tag'], uid, data['tier'], int(data['rating']), rank(data['tier'])) for uid, data in state.items()] - ranked.sort(key=lambda x:(x[-1], x[-2])) #prolly easier just to sort by mmr - await self.send_message( - self.fetch_channel('general'), # for now - "It's that time again, folks!\n" - "The current Overwatch season has come to an end. Let's see how well all of you did, shall we?" - ) - index = { - ranked[i][0]:postfix(str(len(ranked)-i)) for i in range(len(ranked)) - } - for tag,uid,tier,rating,rn in ranked: + async with Database('stats.json') as state: + if len(state): + for uid, data in state.items(): + tag = data['tag'] + rating = data['rating'] + old_tier = data['tier'] if 'tier' in data else 'Unranked' + try: + current, img, tier = get_mmr(tag) + state[uid]['rating'] = current + state[uid]['avatar'] = img + state[uid]['tier'] = tier + except RequestException: + pass + ranked = [(data['tag'], uid, data['tier'], int(data['rating']), rank(data['tier'])) for uid, data in state.items()] + ranked.sort(key=lambda x:(x[-1], x[-2])) #prolly easier just to sort by mmr await self.send_message( - self.fetch_channel('general'), - "In "+index[tag]+" place, "+ - (self.users[uid]['mention'] if uid in self.users else tag)+ - " who made "+tier+ - " with a rating of "+str(rating)+"\n" - +encourage(rn) + ( - ('\n'+state[uid]['avatar']) if 'avatar' in state[uid] - else '' + self.fetch_channel('general'), # for now + "It's that time again, folks!\n" + "The current Overwatch season has come to an end. Let's see how well all of you did, shall we?" + ) + index = { + ranked[i][0]:postfix(str(len(ranked)-i)) for i in range(len(ranked)) + } + for tag,uid,tier,rating,rn in ranked: + await self.send_message( + self.fetch_channel('general'), + "In "+index[tag]+" place, "+ + (self.users[uid]['mention'] if uid in self.users else tag)+ + " who made "+tier+ + " with a rating of "+str(rating)+"\n" + +encourage(rn) + ( + ('\n'+state[uid]['avatar']) if 'avatar' in state[uid] + else '' + ) ) + await self.send_message( + self.fetch_channel('general'), + "Let's give everyone a round of applause. Great show from everybody!\n" + "I can't wait to see how you all do next time! [Competitive ranks reset]" ) - await self.send_message( - self.fetch_channel('general'), - "Let's give everyone a round of applause. Great show from everybody!\n" - "I can't wait to see how you all do next time! [Competitive ranks reset]" - ) - for uid in state: - state[uid]['rating'] = 0 - state[uid]['tier'] = 'Unranked' - save_db(state, 'stats_interim.json') + for uid in state: + state[uid]['rating'] = 0 + state[uid]['tier'] = 'Unranked' + await sate.save_to('stats_interim.json') if os.path.isfile('stats.json'): os.remove('stats.json') @@ -218,20 +218,20 @@ async def cmd_owinit(self, message, content): shutil.move('stats_interim.json', 'stats.json') body = "The new Overwatch season has started! Here are the users I'm " body += "currently tracking statistics for:\n" - stats = load_db('stats.json') - for uid in stats: - body += '%s as %s\n' % ( - self.users[uid]['name'] if uid in self.users else 'someone', - stats[uid]['tag'] + async with Database('stats.json') as stats: + for uid in stats: + body += '%s as %s\n' % ( + self.users[uid]['name'] if uid in self.users else 'someone', + stats[uid]['tag'] + ) + stats[uid]['rating'] = 0 + stats[uid]['tier'] = 'Unranked' + body += "If anyone else would like to be tracked, use the `!ow` command." + body += " Good luck to you all!" + await self.send_message( + self.fetch_channel('general'), + body ) - stats[uid]['rating'] = 0 - stats[uid]['tier'] = 'Unranked' - body += "If anyone else would like to be tracked, use the `!ow` command." - body += " Good luck to you all!" - await self.send_message( - self.fetch_channel('general'), - body - ) - save_db(stats, 'stats.json') + stats.save(0) return bot diff --git a/bots/party.py b/bots/party.py index d3dbb25..db9a8f2 100644 --- a/bots/party.py +++ b/bots/party.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import load_db, save_db, sanitize +from .utils import Database, sanitize import discord from discord.http import Route import asyncio @@ -20,156 +20,156 @@ async def cmd_party(self, message, content): Example: `!party` or `!party Birthday` """ if message.server is not None: - parties = load_db('parties.json', []) - current_party = None - for i in range(len(parties)): - if message.server.id == parties[i]['server'] and message.author.id == parties[i]['creator'] and time.time()-parties[i]['time'] < 86400: - if not parties[i]['primed']: - current_party = parties[i]['name'] - parties[i]['primed'] = True - else: - try: - await self.delete_channel( - discord.utils.get( - message.server.channels, - id=parties[i]['id'], - type=discord.ChannelType.voice + async with ListDatabase('parties.json') as parties: + current_party = None + for i in range(len(parties)): + if message.server.id == parties[i]['server'] and message.author.id == parties[i]['creator'] and time.time()-parties[i]['time'] < 86400: + if not parties[i]['primed']: + current_party = parties[i]['name'] + parties[i]['primed'] = True + else: + try: + await self.delete_channel( + discord.utils.get( + message.server.channels, + id=parties[i]['id'], + type=discord.ChannelType.voice + ) ) + except AttributeError: + pass + except discord.NotFound: + pass + except discord.HTTPException as e: + print("Error deleting channel:", e.text, e.response) + parties[i] = None + parties.update([party for party in parties if party is not None]) + if current_party: + await self.send_message( + message.channel, + "It looks like you already have a party together right now: `%s`\n" + "However, I can disband that party and create this new one for you.\n" + "If you'd like me to do that, just type the same command again" + % current_party + ) + else: + name = (' '.join(content[1:])+' Party') if len(content) > 1 else 'Party' + name = sanitize_channel(name) + party_names = {party['name'] for party in parties} + if name in party_names or name == 'Party': + suffix = 1 + name += ' ' + while name+str(suffix) in party_names: + suffix += 1 + name += str(suffix) + perms = [] + #translate permissions from the text channel where the command was used + #into analogous voice permissions + if hasattr(message.channel, 'overwrites'): + for role, src in message.channel.overwrites: + dest = discord.PermissionOverwrite( + create_instant_invite=src.create_instant_invite, + manage_channels=src.manage_channels, + manage_roles=src.manage_roles, + manage_webhooks=src.manage_webhooks, + connect=src.read_messages, + send=src.send_messages, + mute_members=src.manage_messages, + deafen_members=src.manage_messages, + move_members=src.manage_messages, + use_voice_activation=True ) - except AttributeError: - pass - except discord.NotFound: - pass - except discord.HTTPException as e: - print("Error deleting channel:", e.text, e.response) - parties[i] = None - parties = [party for party in parties if party is not None] - if current_party: - await self.send_message( - message.channel, - "It looks like you already have a party together right now: `%s`\n" - "However, I can disband that party and create this new one for you.\n" - "If you'd like me to do that, just type the same command again" - % current_party - ) - else: - name = (' '.join(content[1:])+' Party') if len(content) > 1 else 'Party' - name = sanitize_channel(name) - party_names = {party['name'] for party in parties} - if name in party_names or name == 'Party': - suffix = 1 - name += ' ' - while name+str(suffix) in party_names: - suffix += 1 - name += str(suffix) - perms = [] - #translate permissions from the text channel where the command was used - #into analogous voice permissions - if hasattr(message.channel, 'overwrites'): - for role, src in message.channel.overwrites: - dest = discord.PermissionOverwrite( - create_instant_invite=src.create_instant_invite, - manage_channels=src.manage_channels, - manage_roles=src.manage_roles, - manage_webhooks=src.manage_webhooks, - connect=src.read_messages, - send=src.send_messages, - mute_members=src.manage_messages, - deafen_members=src.manage_messages, - move_members=src.manage_messages, - use_voice_activation=True + perms.append((role, dest)) + # Add specific override for Beymax (so he can kill the channel) + perms.append(( + message.server.get_member(self.user.id), + discord.PermissionOverwrite( + manage_channels=True ) - perms.append((role, dest)) - # Add specific override for Beymax (so he can kill the channel) - perms.append(( - message.server.get_member(self.user.id), - discord.PermissionOverwrite( - manage_channels=True - ) - )) - # Add specific override for the channel's creator (so they can modify permissions) - perms.append(( - message.author, - discord.PermissionOverwrite( - manage_roles=True, - manage_channels=True # Allow creator to modify the channel - ) - )) - # FIXME: discord.py needs to add category support - # channel = await self.create_channel( - # message.server, - # name, - # *perms, - # type=discord.ChannelType.voice, - # category=self.categories['Voice Channels'] - # ) + )) + # Add specific override for the channel's creator (so they can modify permissions) + perms.append(( + message.author, + discord.PermissionOverwrite( + manage_roles=True, + manage_channels=True # Allow creator to modify the channel + ) + )) + # FIXME: discord.py needs to add category support + # channel = await self.create_channel( + # message.server, + # name, + # *perms, + # type=discord.ChannelType.voice, + # category=self.categories['Voice Channels'] + # ) - #Temporary workaround for party creation within categories - target_category = None - category_reference = self.config_get('party_category') - if category_reference is not None: - for channel in message.server.channels: - #FIXME: CategoryType instead of 4 - if channel.type == 4 and ( - channel.id == category_reference or - channel.name == category_reference - ): - target_category = channel.id - if target_category is None: - raise NameError("No category '%s'"%category_reference) + #Temporary workaround for party creation within categories + target_category = None + category_reference = self.config_get('party_category') + if category_reference is not None: + for channel in message.server.channels: + #FIXME: CategoryType instead of 4 + if channel.type == 4 and ( + channel.id == category_reference or + channel.name == category_reference + ): + target_category = channel.id + if target_category is None: + raise NameError("No category '%s'"%category_reference) - @asyncio.coroutine - def tmp_create_channel(): - permissions_payload = [ - { - 'allow': rule.pair()[0].value, - 'deny': rule.pair()[1].value, - 'id': target.id, - 'type': 'member' if isinstance(target, discord.User) else 'role' - } - for target, rule in perms - ] + @asyncio.coroutine + def tmp_create_channel(): + permissions_payload = [ + { + 'allow': rule.pair()[0].value, + 'deny': rule.pair()[1].value, + 'id': target.id, + 'type': 'member' if isinstance(target, discord.User) else 'role' + } + for target, rule in perms + ] - def tmp_post_request(): - payload = { - 'name': name, - 'type': str(discord.ChannelType.voice), - 'permission_overwrites': permissions_payload, - 'parent_id': target_category - } - return self.http.request( - Route( - 'POST', - '/guilds/{guild_id}/channels', - guild_id=message.server.id - ), - json=payload, - # reason=None - ) - data = yield from tmp_post_request() - channel = discord.Channel(server=message.server, **data) - return channel + def tmp_post_request(): + payload = { + 'name': name, + 'type': str(discord.ChannelType.voice), + 'permission_overwrites': permissions_payload, + 'parent_id': target_category + } + return self.http.request( + Route( + 'POST', + '/guilds/{guild_id}/channels', + guild_id=message.server.id + ), + json=payload, + # reason=None + ) + data = yield from tmp_post_request() + channel = discord.Channel(server=message.server, **data) + return channel - channel = await tmp_create_channel() - await self.send_message( - message.channel, - "Alright, %s, I've created the `%s` channel for you.\n" - "When you're finished, you can close the channel with `!disband`\n" - "Otherwise, I'll go ahead and close it for you after 24 hours, if nobody's using it" - % ( - message.author.mention, - name + channel = await tmp_create_channel() + await self.send_message( + message.channel, + "Alright, %s, I've created the `%s` channel for you.\n" + "When you're finished, you can close the channel with `!disband`\n" + "Otherwise, I'll go ahead and close it for you after 24 hours, if nobody's using it" + % ( + message.author.mention, + name + ) ) - ) - parties.append({ - 'name':name, - 'id':channel.id, - 'server':message.server.id, - 'primed':False, - 'creator':message.author.id, - 'time': time.time() - }) - save_db(parties, 'parties.json') + parties.append({ + 'name':name, + 'id':channel.id, + 'server':message.server.id, + 'primed':False, + 'creator':message.author.id, + 'time': time.time() + }) + parties.save() else: await self.send_message( message.channel, @@ -183,30 +183,30 @@ async def cmd_disband(self, message, content): `!disband` : Closes any active party voice channels you have """ if message.server is not None: - parties = load_db('parties.json', []) - pruned = [] - for i in range(len(parties)): - if message.server.id == parties[i]['server'] and message.author.id == parties[i]['creator']: - channel = discord.utils.get( - self.get_all_channels(), - id=parties[i]['id'], - type=discord.ChannelType.voice - ) - if channel is not None: - pruned.append( - '`%s`' % parties[i]['name'] - if str(parties[i]['name']) == str(channel.name) - else '`%s` AKA `%s`' % ( - channel.name, - parties[i]['name'] - ) - ) - await self.delete_channel( - channel + async with ListDatabase('parties.json') as parties: + pruned = [] + for i in range(len(parties)): + if message.server.id == parties[i]['server'] and message.author.id == parties[i]['creator']: + channel = discord.utils.get( + self.get_all_channels(), + id=parties[i]['id'], + type=discord.ChannelType.voice ) - parties[i] = None - parties = [party for party in parties if party is not None] - save_db(parties, 'parties.json') + if channel is not None: + pruned.append( + '`%s`' % parties[i]['name'] + if str(parties[i]['name']) == str(channel.name) + else '`%s` AKA `%s`' % ( + channel.name, + parties[i]['name'] + ) + ) + await self.delete_channel( + channel + ) + parties[i] = None + parties.update([party for party in parties if party is not None]) + parties.save() if len(pruned) == 1: await self.send_message( message.channel, @@ -229,31 +229,31 @@ async def cmd_disband(self, message, content): @bot.add_task(600) # 10 min async def prune_parties(self): current = time.time() - parties = load_db('parties.json', []) - pruned = [] - for i in range(len(parties)): - if current - parties[i]['time'] >= 86400: # 24 hours - channel = discord.utils.get( - self.get_all_channels(), - id=parties[i]['id'], - type=discord.ChannelType.voice - ) - if channel is None or not len(channel.voice_members): - if channel is not None: - pruned.append( - '`%s`' % parties[i]['name'] - if str(parties[i]['name']) == str(channel.name) - else '`%s` AKA `%s`' % ( - channel.name, - parties[i]['name'] + async with ListDatabase('parties.json') as parties: + pruned = [] + for i in range(len(parties)): + if current - parties[i]['time'] >= 86400: # 24 hours + channel = discord.utils.get( + self.get_all_channels(), + id=parties[i]['id'], + type=discord.ChannelType.voice + ) + if channel is None or not len(channel.voice_members): + if channel is not None: + pruned.append( + '`%s`' % parties[i]['name'] + if str(parties[i]['name']) == str(channel.name) + else '`%s` AKA `%s`' % ( + channel.name, + parties[i]['name'] + ) ) - ) - await self.delete_channel( - channel - ) - parties[i] = None - parties = [party for party in parties if party is not None] - save_db(parties, 'parties.json') + await self.delete_channel( + channel + ) + parties[i] = None + parties.update([party for party in parties if party is not None]) + parties.save() if len(pruned) == 1: await self.send_message( self.fetch_channel('general'), diff --git a/bots/story.py b/bots/story.py index 286ffbf..0e4041a 100644 --- a/bots/story.py +++ b/bots/story.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import getname, load_db, save_db +from .utils import getname, Database import discord import asyncio import os @@ -141,71 +141,71 @@ def checker(self, message): @bot.add_special(checker) async def state_router(self, message, content): # Routes messages depending on the game state - state = load_db('game.json', {'user':'~'}) - if state['user'] == message.author.id: - if not hasattr(self, 'player'): - # The game has been interrupted - await self.send_message( - message.channel, - "Resuming game in progress...\n" - "Please wait" - ) - self.player = Player(state['game']) - for msg in state['transcript']: - self.player.write(msg) - await asyncio.sleep(0.5) + async with Database('game.json', {'user':'~'}) as state: + if state['user'] == message.author.id: + if not hasattr(self, 'player'): + # The game has been interrupted + await self.send_message( + message.channel, + "Resuming game in progress...\n" + "Please wait" + ) + self.player = Player(state['game']) + for msg in state['transcript']: + self.player.write(msg) + await asyncio.sleep(0.5) + self.player.readchunk() + content = message.content.strip().lower() + if content == '$': + content = '\n' + state['transcript'].append(content) + state.save() + self.player.write('\n') + await self.send_message( + message.channel, + '```'+self.player.readchunk()+'```' + ) + elif content == 'score': + self.player.write('score') self.player.readchunk() - content = message.content.strip().lower() - if content == '$': - content = '\n' - state['transcript'].append(content) - save_db(state, 'game.json') - self.player.write('\n') - await self.send_message( - message.channel, - '```'+self.player.readchunk()+'```' - ) - elif content == 'score': - self.player.write('score') - self.player.readchunk() - await self.send_message( - message.channel, - 'Your score is %d' % self.player.score - ) - elif content == 'quit': - self.player.write('score') - self.player.readchunk() - self.player.quit() - await self.send_message( - message.channel, - 'You have quit your game. Your score was %d' % self.player.score - ) - state['user'] = '~' - print("Granting xp for score payout") - self.dispatch( - 'grant_xp', - message.author, - self.player.score * 10 #maybe normalize this since each game scores differently - ) - del state['transcript'] - del self.player - save_db(state, 'game.json') + await self.send_message( + message.channel, + 'Your score is %d' % self.player.score + ) + elif content == 'quit': + self.player.write('score') + self.player.readchunk() + self.player.quit() + await self.send_message( + message.channel, + 'You have quit your game. Your score was %d' % self.player.score + ) + state['user'] = '~' + print("Granting xp for score payout") + self.dispatch( + 'grant_xp', + message.author, + self.player.score * 10 #maybe normalize this since each game scores differently + ) + del state['transcript'] + del self.player + state.save() + else: + state['transcript'].append(content) + state.save() + self.player.write(content) + await self.send_message( + message.channel, + '```'+self.player.readchunk()+'```' + ) else: - state['transcript'].append(content) - save_db(state, 'game.json') - self.player.write(content) await self.send_message( - message.channel, - '```'+self.player.readchunk()+'```' + message.author, + "Please refrain from posting messages in the story channel" + " while someone else is playing" ) - else: - await self.send_message( - message.author, - "Please refrain from posting messages in the story channel" - " while someone else is playing" - ) - await asyncio.sleep(0.5) - await self.delete_message(message) + await asyncio.sleep(0.5) + await self.delete_message(message) @bot.add_command('!_start') async def cmd_start(self, message, content): @@ -213,56 +213,56 @@ async def cmd_start(self, message, content): `!_start ` : Starts an interactive text adventure Example: `!_start zork1` """ - state = load_db('game.json', {'user':'~'}) - if state['user'] == '~': - games = { - f[:-3] for f in os.listdir('games') if f.endswith('.z5') - } - if content[1] in games: - state['user'] = message.author.id - state['transcript'] = [] - state['game'] = content[1] - save_db(state, 'game.json') - self.player = Player(content[1]) - # in future: - # See if there's a way to change permissions of an existing channel - # For now, just delete other player's messages - await self.send_message( - message.author, - 'Here are the controls for the story-mode system:\n' - 'Any message you type in the story channel will be interpreted' - ' as input to the game **unless** your message starts with `!`' - ' (my commands)\n' - '`$` : Simply type `$` to enter a blank line to the game\n' - '`quit` : Quits the game in progress\n' - '`score` : View your score\n' - 'Some games may have their own commands in addition to these' - ' ones that I handle personally' - ) - await self.send_message( - self.fetch_channel('story'), - '%s is now playing %s\n' - 'The game will begin shortly' % ( - message.author.mention, - content[1] + async with Database('game.json', {'user':'~'}) as state: + if state['user'] == '~': + games = { + f[:-3] for f in os.listdir('games') if f.endswith('.z5') + } + if content[1] in games: + state['user'] = message.author.id + state['transcript'] = [] + state['game'] = content[1] + state.save() + self.player = Player(content[1]) + # in future: + # See if there's a way to change permissions of an existing channel + # For now, just delete other player's messages + await self.send_message( + message.author, + 'Here are the controls for the story-mode system:\n' + 'Any message you type in the story channel will be interpreted' + ' as input to the game **unless** your message starts with `!`' + ' (my commands)\n' + '`$` : Simply type `$` to enter a blank line to the game\n' + '`quit` : Quits the game in progress\n' + '`score` : View your score\n' + 'Some games may have their own commands in addition to these' + ' ones that I handle personally' + ) + await self.send_message( + self.fetch_channel('story'), + '%s is now playing %s\n' + 'The game will begin shortly' % ( + message.author.mention, + content[1] + ) + ) + # Post to general + await asyncio.sleep(2) + await self.send_message( + self.fetch_channel('story'), + '```'+self.player.readchunk()+'```' + ) + else: + await self.send_message( + message.channel, + "That is not a valid game" ) - ) - # Post to general - await asyncio.sleep(2) - await self.send_message( - self.fetch_channel('story'), - '```'+self.player.readchunk()+'```' - ) else: await self.send_message( message.channel, - "That is not a valid game" + "Please wait until the current player finishes their game" ) - else: - await self.send_message( - message.channel, - "Please wait until the current player finishes their game" - ) def xp_for(level): @@ -278,51 +278,51 @@ async def grant_some_xp(self, evt, user, xp): xp, str(user) ) ) - players = load_db('players.json') - if user.id not in players: - players[user.id] = { - 'level':1, - 'xp':0, - 'balance':10 - } - player = players[user.id] - player['xp'] += xp - current_level = player['level'] - while player['xp'] >= xp_for(player['level']+1): - player['xp'] -= xp_for(player['level']+1) - player['level'] += 1 - if player['level'] > current_level: - await self.send_message( - user, - "Congratulations on reaching level %d! Your weekly token payout" - " and maximum token balance have both been increased. To check" - " your balance, type `!balance`" % player['level'] - ) - players[user.id] = player - save_db(players, 'players.json') + async with Database('players.json') as players: + if user.id not in players: + players[user.id] = { + 'level':1, + 'xp':0, + 'balance':10 + } + player = players[user.id] + player['xp'] += xp + current_level = player['level'] + while player['xp'] >= xp_for(player['level']+1): + player['xp'] -= xp_for(player['level']+1) + player['level'] += 1 + if player['level'] > current_level: + await self.send_message( + user, + "Congratulations on reaching level %d! Your weekly token payout" + " and maximum token balance have both been increased. To check" + " your balance, type `!balance`" % player['level'] + ) + players[user.id] = player + players.save() @bot.add_command('!balance') async def cmd_balance(self, message, content): """ `!balance` : Displays your current token balance """ - players = load_db('players.json') - if message.author.id not in players: - players[message.author.id] = { - 'level':1, - 'xp':0, - 'balance':10 - } - player = players[message.author.id] - await self.send_message( - message.author, - "You are currently level %d and have a balance of %d tokens\n" - "You have %d xp to go to reach the next level" % ( - player['level'], - player['balance'], - xp_for(player['level']+1)-player['xp'] + async with Database('players.json') as players: + if message.author.id not in players: + players[message.author.id] = { + 'level':1, + 'xp':0, + 'balance':10 + } + player = players[message.author.id] + await self.send_message( + message.author, + "You are currently level %d and have a balance of %d tokens\n" + "You have %d xp to go to reach the next level" % ( + player['level'], + player['balance'], + xp_for(player['level']+1)-player['xp'] + ) ) - ) @bot.add_command('!_bid') async def cmd_bid(self, message, content): @@ -333,27 +333,27 @@ async def cmd_bid(self, message, content): @bot.subscribe('command') async def record_command(self, evt, command, user): - week = load_db('weekly.json') - if user.id not in week: - week[user.id] = {} - print(week) - if 'commands' not in week[user.id]: - week[user.id]['commands'] = [command] - print("granting xp for first command", command) - self.dispatch( - 'grant_xp', - user, - 5 - ) - elif command not in week[user.id]['commands']: - week[user.id]['commands'].append(command) - print("granting xp for new command", command) - self.dispatch( - 'grant_xp', - user, - 5 - ) - save_db(week, 'weekly.json') + async with Database('weekly.json') as week: + if user.id not in week: + week[user.id] = {} + print(week) + if 'commands' not in week[user.id]: + week[user.id]['commands'] = [command] + print("granting xp for first command", command) + self.dispatch( + 'grant_xp', + user, + 5 + ) + elif command not in week[user.id]['commands']: + week[user.id]['commands'].append(command) + print("granting xp for new command", command) + self.dispatch( + 'grant_xp', + user, + 5 + ) + week.save() @bot.subscribe('after:message') async def record_activity(self, evt, message): @@ -361,55 +361,55 @@ async def record_activity(self, evt, message): @bot.subscribe('cleanup') async def save_activity(self, evt): - week = load_db('weekly') - print(week, self._pending_activity) - for uid in self._pending_activity: - if uid not in week: - week[uid]={'active':'yes'} - else: - week[uid]['active']='yes' - self._pending_activity = set() - print(week) - save_db(week, 'weekly.json') + async with Database('weekly.json') as week: + print(week, self._pending_activity) + for uid in self._pending_activity: + if uid not in week: + week[uid]={'active':'yes'} + else: + week[uid]['active']='yes' + self._pending_activity = set() + print(week) + week.save() @bot.add_task(604800) # 1 week async def reset_week(self): #{uid: {}} - week = load_db('weekly.json') - players = load_db('players.json') - print("Resetting the week") - xp = [] - for uid in week: - user = self.fetch_channel('story').server.get_member(uid) #icky! - if 'active' in week[uid] or uid in self._pending_activity: - xp.append([user, 5]) - if uid not in players: - players[uid] = { - 'level':1, - 'xp':0, - 'balance':10 - } - payout = players[user.id]['level'] - if players[user.id]['balance'] < 20*players[user.id]['level']: - payout *= 2 - players[uid]['balance'] += payout - await self.send_message( - self.fetch_channel('story').server.get_member(uid), #icky! - "Your allowance was %d tokens this week. Your balance is now %d " - "tokens" % ( - payout, - players[uid]['balance'] - ) - ) - self._pending_activity = set() - save_db(players, 'players.json') - save_db({}, 'weekly.json') - for user, payout in xp: - print("granting xp for activity payout") - self.dispatch( - 'grant_xp', - user, - payout - ) + async with Database('players.json') as players: + async with Database('weekly.json') as weekly: + print("Resetting the week") + xp = [] + for uid in week: + user = self.fetch_channel('story').server.get_member(uid) #icky! + if 'active' in week[uid] or uid in self._pending_activity: + xp.append([user, 5]) + if uid not in players: + players[uid] = { + 'level':1, + 'xp':0, + 'balance':10 + } + payout = players[user.id]['level'] + if players[user.id]['balance'] < 20*players[user.id]['level']: + payout *= 2 + players[uid]['balance'] += payout + await self.send_message( + self.fetch_channel('story').server.get_member(uid), #icky! + "Your allowance was %d tokens this week. Your balance is now %d " + "tokens" % ( + payout, + players[uid]['balance'] + ) + ) + self._pending_activity = set() + players.save() + os.remove('weekly.json') + for user, payout in xp: + print("granting xp for activity payout") + self.dispatch( + 'grant_xp', + user, + payout + ) return bot diff --git a/bots/utils.py b/bots/utils.py index f12ba1c..26656ef 100644 --- a/bots/utils.py +++ b/bots/utils.py @@ -1,7 +1,96 @@ import json import sys +import asyncio +import warnings + +db_lock = asyncio.Lock() +locks = {} + +class Database(dict): + def __init__(self, filename, default=None): + super().__init__(self) + if default is not none and not isinstance(default, dict): + raise TypeError("Cannot use a Database object on non-dictionary type") + self.filename = filename + self.default=default + + async def __aenter__(self): + global db_lock + global locks + async with db_lock: + if self.filename not in locks: + locks[self.filename] = asyncio.Lock() + await locks[self.filename].acquire() + try: + with open(self.filename) as reader: + self.update(json.load(reader)) + except FileNotFoundError: + self.update({} if self.default is None else self.default) + return self + + def save(self): + with open(self.filename, 'w') as writer: + return json.dump(self, writer) + + async def save_to(self, filename): + async with Database(filename) as tmp: + for k in list(tmp): + del tmp[k] + tmp.update(self) + tmp.save() + + async def __aexit__(self, *args): + global locks + locks[self.filename].release() + +class ListDatabase(list): + def __init__(self, filename, default=None): + super().__init__(self) + if default is not none and not isinstance(default, list): + raise TypeError("Cannot use a ListDatabase object on non-list type") + self.filename = filename + self.default=default + + async def __aenter__(self): + global db_lock + global locks + async with db_lock: + if self.filename not in locks: + locks[self.filename] = asyncio.Lock() + await locks[self.filename].acquire() + try: + with open(self.filename) as reader: + self += json.load(reader) + except FileNotFoundError: + self += ([] if self.default is None else self.default) + return self + + def save(self): + with open(self.filename, 'w') as writer: + return json.dump(self, writer) + + async def save_to(self, filename): + async with ListDatabase(filename) as tmp: + while len(tmp): + tmp.pop() + tmp += self + tmp.save() + + def update(self, data): + while len(self): + self.pop() + self += [item for item in data] + + async def __aexit__(self, *args): + global locks + locks[self.filename].release() def load_db(filename, default=None): + warnings.warn( + "load_db is deprecated as it is not async safe", + warnings.DeprecationWarning, + 2 + ) try: with open(filename) as reader: return json.load(reader) diff --git a/main.py b/main.py index 065858b..e5786b8 100644 --- a/main.py +++ b/main.py @@ -70,8 +70,11 @@ async def ready_up(self, event): print(channel.name, channel.type) print("Ready to serve!") self.dispatch('task:update_status') # manually set status at startup - from bots.utils import load_db - print(load_db('weekly.json')) + from bots.utils import Database + async with Database('weekly.json') as weekly: + print(weekly) + weekly['test'] = 7 + weekly.save() @beymax.subscribe('member_join') async def greet_member(self, event, member): #greet new members From 236849ae034b9282b813d2c05bcc1dcd9e148a6a Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 13:32:56 -0500 Subject: [PATCH 13/23] Fixed database synchronization --- bots/core.py | 2 +- bots/party.py | 2 +- bots/story.py | 31 ++++++++++++++++--------------- bots/utils.py | 6 +++--- main.py | 2 -- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/bots/core.py b/bots/core.py index 4168c05..c970aac 100644 --- a/bots/core.py +++ b/bots/core.py @@ -1,4 +1,4 @@ -from .utils import Database, getname, validate_permissions +from .utils import load_db, save_db, Database, getname, validate_permissions import discord from discord.compat import create_task import asyncio diff --git a/bots/party.py b/bots/party.py index db9a8f2..0984a3a 100644 --- a/bots/party.py +++ b/bots/party.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import Database, sanitize +from .utils import ListDatabase, sanitize import discord from discord.http import Route import asyncio diff --git a/bots/story.py b/bots/story.py index 0e4041a..ba43dc0 100644 --- a/bots/story.py +++ b/bots/story.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import getname, Database +from .utils import getname, Database, load_db, save_db import discord import asyncio import os @@ -181,7 +181,7 @@ async def state_router(self, message, content): 'You have quit your game. Your score was %d' % self.player.score ) state['user'] = '~' - print("Granting xp for score payout") + # print("Granting xp for score payout") self.dispatch( 'grant_xp', message.author, @@ -273,11 +273,11 @@ def xp_for(level): @bot.subscribe('grant_xp') async def grant_some_xp(self, evt, user, xp): - print( - ": %d xp has been granted to %s" % ( - xp, str(user) - ) - ) + # print( + # ": %d xp has been granted to %s" % ( + # xp, str(user) + # ) + # ) async with Database('players.json') as players: if user.id not in players: players[user.id] = { @@ -336,10 +336,10 @@ async def record_command(self, evt, command, user): async with Database('weekly.json') as week: if user.id not in week: week[user.id] = {} - print(week) + # print(week) if 'commands' not in week[user.id]: week[user.id]['commands'] = [command] - print("granting xp for first command", command) + # print("granting xp for first command", command) self.dispatch( 'grant_xp', user, @@ -347,7 +347,7 @@ async def record_command(self, evt, command, user): ) elif command not in week[user.id]['commands']: week[user.id]['commands'].append(command) - print("granting xp for new command", command) + # print("granting xp for new command", command) self.dispatch( 'grant_xp', user, @@ -357,26 +357,27 @@ async def record_command(self, evt, command, user): @bot.subscribe('after:message') async def record_activity(self, evt, message): - self._pending_activity.add(message.author.id) + if message.author.id != self.user.id: + self._pending_activity.add(message.author.id) @bot.subscribe('cleanup') async def save_activity(self, evt): async with Database('weekly.json') as week: - print(week, self._pending_activity) + # print(week, self._pending_activity) for uid in self._pending_activity: if uid not in week: week[uid]={'active':'yes'} else: week[uid]['active']='yes' self._pending_activity = set() - print(week) + # print(week) week.save() @bot.add_task(604800) # 1 week async def reset_week(self): #{uid: {}} async with Database('players.json') as players: - async with Database('weekly.json') as weekly: + async with Database('weekly.json') as week: print("Resetting the week") xp = [] for uid in week: @@ -405,7 +406,7 @@ async def reset_week(self): players.save() os.remove('weekly.json') for user, payout in xp: - print("granting xp for activity payout") + # print("granting xp for activity payout") self.dispatch( 'grant_xp', user, diff --git a/bots/utils.py b/bots/utils.py index 26656ef..a6ef85d 100644 --- a/bots/utils.py +++ b/bots/utils.py @@ -9,7 +9,7 @@ class Database(dict): def __init__(self, filename, default=None): super().__init__(self) - if default is not none and not isinstance(default, dict): + if default is not None and not isinstance(default, dict): raise TypeError("Cannot use a Database object on non-dictionary type") self.filename = filename self.default=default @@ -46,7 +46,7 @@ async def __aexit__(self, *args): class ListDatabase(list): def __init__(self, filename, default=None): super().__init__(self) - if default is not none and not isinstance(default, list): + if default is not None and not isinstance(default, list): raise TypeError("Cannot use a ListDatabase object on non-list type") self.filename = filename self.default=default @@ -88,7 +88,7 @@ async def __aexit__(self, *args): def load_db(filename, default=None): warnings.warn( "load_db is deprecated as it is not async safe", - warnings.DeprecationWarning, + DeprecationWarning, 2 ) try: diff --git a/main.py b/main.py index e5786b8..f21096e 100644 --- a/main.py +++ b/main.py @@ -73,8 +73,6 @@ async def ready_up(self, event): from bots.utils import Database async with Database('weekly.json') as weekly: print(weekly) - weekly['test'] = 7 - weekly.save() @beymax.subscribe('member_join') async def greet_member(self, event, member): #greet new members From 9764d9d37827b554483903fc7fa82c24c30b2e5e Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 13:45:11 -0500 Subject: [PATCH 14/23] Finished XP backend --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index f21096e..5755916 100644 --- a/main.py +++ b/main.py @@ -72,7 +72,7 @@ async def ready_up(self, event): self.dispatch('task:update_status') # manually set status at startup from bots.utils import Database async with Database('weekly.json') as weekly: - print(weekly) + print("Weekly xp structure:", weekly) @beymax.subscribe('member_join') async def greet_member(self, event, member): #greet new members @@ -123,7 +123,7 @@ async def react(self, message, content): message, b'\xf0\x9f\x91\x8d'.decode() if random.random() < 0.8 else b'\xf0\x9f\x8d\x86'.decode() # :thumbsup: and sometimes :eggplant: ) - print("granting reaction xp") + # print("granting reaction xp") self.dispatch( 'grant_xp', message.author, From d63775ddcd6ca902b6b4b072f08612551fcc9f1c Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 14:54:01 -0500 Subject: [PATCH 15/23] Finished economy implementation --- bots/story.py | 266 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 213 insertions(+), 53 deletions(-) diff --git a/bots/story.py b/bots/story.py index ba43dc0..86dea8a 100644 --- a/bots/story.py +++ b/bots/story.py @@ -6,6 +6,7 @@ import subprocess import queue import threading +import time import re more_patterns = [ @@ -118,10 +119,10 @@ def EnableStory(bot): bot.reserve_channel('story') bot._pending_activity = set() - @bot.add_command('!_stories') + @bot.add_command('!games') async def cmd_story(self, message, content): """ - `!_stories` : Lists the available stories + `!games` : Lists the available games """ games = [ f[:-3] for f in os.listdir('games') if f.endswith('.z5') @@ -129,7 +130,7 @@ async def cmd_story(self, message, content): await self.send_message( message.channel, '\n'.join( - ["Here are the stories thar are available:"]+ + ["Here are the games that are available:"]+ games ) ) @@ -173,23 +174,46 @@ async def state_router(self, message, content): 'Your score is %d' % self.player.score ) elif content == 'quit': - self.player.write('score') - self.player.readchunk() - self.player.quit() - await self.send_message( - message.channel, - 'You have quit your game. Your score was %d' % self.player.score - ) - state['user'] = '~' - # print("Granting xp for score payout") - self.dispatch( - 'grant_xp', - message.author, - self.player.score * 10 #maybe normalize this since each game scores differently - ) + async with Database('players.json') as players: + if 'played' in state and not state['played']: + await self.send_message( + message.channel, + "You quit your game without playing. " + "You are being refunded %d tokens" % ( + state['refund'] + ) + ) + players[message.author.id]['balance'] += state['refund'] + else: + self.player.write('score') + self.player.readchunk() + self.player.quit() + await self.send_message( + message.channel, + 'You have quit your game. Your score was %d\n' + 'Thanks for playing! You will receive %d tokens' % ( + self.player.score, + self.player.score + ) + ) + players[message.author.id]['balance'] += self.player.score + state['user'] = '~' + # print("Granting xp for score payout") + self.dispatch( + 'grant_xp', + message.author, + self.player.score * 10 #maybe normalize this since each game scores differently + ) del state['transcript'] del self.player state.save() + if 'bids' not in state or len(state['bids']) == 1: + await self.send_message( + self.fetch_channel('story'), + "The game is now idle and will be awarded to the first bidder" + ) + else: + self.dispatch('startgame') else: state['transcript'].append(content) state.save() @@ -219,40 +243,13 @@ async def cmd_start(self, message, content): f[:-3] for f in os.listdir('games') if f.endswith('.z5') } if content[1] in games: - state['user'] = message.author.id - state['transcript'] = [] - state['game'] = content[1] + state['bids'] = [{ + 'user':message.author.id, + 'game':content[1], + 'amount':0 + }] state.save() - self.player = Player(content[1]) - # in future: - # See if there's a way to change permissions of an existing channel - # For now, just delete other player's messages - await self.send_message( - message.author, - 'Here are the controls for the story-mode system:\n' - 'Any message you type in the story channel will be interpreted' - ' as input to the game **unless** your message starts with `!`' - ' (my commands)\n' - '`$` : Simply type `$` to enter a blank line to the game\n' - '`quit` : Quits the game in progress\n' - '`score` : View your score\n' - 'Some games may have their own commands in addition to these' - ' ones that I handle personally' - ) - await self.send_message( - self.fetch_channel('story'), - '%s is now playing %s\n' - 'The game will begin shortly' % ( - message.author.mention, - content[1] - ) - ) - # Post to general - await asyncio.sleep(2) - await self.send_message( - self.fetch_channel('story'), - '```'+self.player.readchunk()+'```' - ) + self.dispatch('startgame') else: await self.send_message( message.channel, @@ -324,12 +321,175 @@ async def cmd_balance(self, message, content): ) ) - @bot.add_command('!_bid') + @bot.add_command('!bid') async def cmd_bid(self, message, content): """ - `!_bid ` : Place a bid to play the next game + `!bid ` : Place a bid to play the next game + Example: `!bid 1 zork1` + """ + async with Database('game.json', {'user':'~'}) as state: + async with Database('players.json') as players: + bid = content[1] + try: + bid = int(bid) + except ValueError: + await self.send_message( + message.channel, + "'%s' is not a valid amount of tokens" % bid + ) + return + game = content[2] + games = { + f[:-3] for f in os.listdir('games') if f.endswith('.z5') + } + if 'bids' not in state: + state['bids'] = [{'user':'', 'amount':1, 'game':''}] + if bid <= state['bids'][-1]['amount']: + if len(state['bids'][-1]['user']): + await self.send_message( + message.channel, + "The current highest bid is %d tokens. Your bid must" + " be at least %d tokens." % ( + state['bids'][-1]['amount'], + state['bids'][-1]['amount'] + 1 + ) + ) + return + else: + await self.send_message( + message.channel, + "The minimum bid is 1 token" + ) + return + if message.author.id not in players: + players[message.author.id] = { + 'level':1, + 'xp':0, + 'balance':10 + } + if bid < players[message.author.id]['balance']: + await self.send_message( + message.channel, + "You do not have enough tokens to make that bid." + "To check your token balance, use `!balance`" + ) + return + if game not in games: + await self.send_message( + message.channel, + "That is not a valid game. To see the list of games that" + " are available, use `!games`" + ) + return + await self.send_message( + message.channel, + "Your bid has been placed. If you are not outbid, your" + " game will begin after the current game has ended" + ) + user = self.fetch_channel('story').server.get_member(state['bids'][-1]['user']) + if user: + await self.send_message( + user, + "You have been outbid by %s with a bid of %d tokens." + " If you would like to place another bid, use " + "`!bid %d %s`" % ( + getname(message.author), + bid, + bid+1, + state['bids'][-1]['game'] + ) + ) + state['bids'].append({ + 'user':message.author.id, + 'amount':bid, + 'game':game + }) + state.save() + if state['user'] == '~': + self.dispatch('startgame') + + @bot.add_command('!reup') + async def cmd_reup(self, message, content): + """ + `!reup` : Extends your current game session by 1 day """ - pass + + @bot.subscribe('startgame') + async def start_game(self, evt): + async with Database('game.json', {'user':'~', 'bids':[]}) as state: + async with Database('players.json') as players: + if state['user'] == '~': + for bid in reversed(state['bids']): + if bid['user'] != '': + if bid['user'] not in players: + players[bid['user']] = { + 'level':1, + 'xp':0, + 'balance':10 + } + user = self.fetch_channel('story').server.get_member(bid['user']) + if bid['amount'] < players[bid['user']]['balance']: + await self.send_message( + user, + "You do not have enough tokens to cover your" + " bid of %d. Your bid is forfeit and the game" + " shall pass to the next highest bidder" % ( + bid['amount'] + ) + ) + continue + players[bid['user']]['balance'] -= bid['amount'] + players.save() + state['user'] = bid['user'] + state['transcript'] = [] + state['game'] = bid['game'] + state['played'] = False + state['refund'] = max(0, bid['amount'] - 1) + state['time'] = time.time() + state['bids'] = [{'user':'', 'amount':1, 'game':''}] + state.save() + self.player = Player(bid['game']) + # in future: + # See if there's a way to change permissions of an existing channel + # For now, just delete other player's messages + await self.send_message( + user, + 'You have up to 2 days to finish your game, after' + ' which, your game will automatically end\n' + 'Here are the controls for the story-mode system:\n' + 'Any message you type in the story channel will be interpreted' + ' as input to the game **unless** your message starts with `!`' + ' (my commands)\n' + '`$` : Simply type `$` to enter a blank line to the game\n' + 'That can be useful if the game is stuck or ' + 'if it ignored your last input\n' + '`quit` : Quits the game in progress\n' + 'This is also how you end the game if you finish it\n' + '`score` : View your score\n' + 'Some games may have their own commands in addition to these' + ' ones that I handle personally' + ) + await self.send_message( + self.fetch_channel('story'), + '%s is now playing %s\n' + 'The game will begin shortly' % ( + user.mention, + content[1] + ) + ) + # Post to general + await asyncio.sleep(2) + await self.send_message( + self.fetch_channel('story'), + '```'+self.player.readchunk()+'```' + ) + return + await self.send_message( + self.fetch_channel('story'), + "None of the bidders for the current game session could" + " honor their bids. The game is now idle and will be" + " awarded to the first bidder" + ) @bot.subscribe('command') async def record_command(self, evt, command, user): From 53fca7ed90e8afdc12d7aed9dd8081a24bfb539c Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 15:30:34 -0500 Subject: [PATCH 16/23] Fixed up economy implementation --- bots/story.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/bots/story.py b/bots/story.py index 86dea8a..3d660dc 100644 --- a/bots/story.py +++ b/bots/story.py @@ -205,6 +205,7 @@ async def state_router(self, message, content): self.player.score * 10 #maybe normalize this since each game scores differently ) del state['transcript'] + state['user'] = '~' del self.player state.save() if 'bids' not in state or len(state['bids']) == 1: @@ -215,6 +216,7 @@ async def state_router(self, message, content): else: self.dispatch('startgame') else: + state['played'] = True state['transcript'].append(content) state.save() self.player.write(content) @@ -343,7 +345,11 @@ async def cmd_bid(self, message, content): f[:-3] for f in os.listdir('games') if f.endswith('.z5') } if 'bids' not in state: - state['bids'] = [{'user':'', 'amount':1, 'game':''}] + state['bids'] = [{'user':'', 'amount':0, 'game':''}] + # print(state) + # print(players) + # print(bid) + # print(game) if bid <= state['bids'][-1]['amount']: if len(state['bids'][-1]['user']): await self.send_message( @@ -367,7 +373,7 @@ async def cmd_bid(self, message, content): 'xp':0, 'balance':10 } - if bid < players[message.author.id]['balance']: + if bid > players[message.author.id]['balance']: await self.send_message( message.channel, "You do not have enough tokens to make that bid." @@ -413,6 +419,39 @@ async def cmd_reup(self, message, content): """ `!reup` : Extends your current game session by 1 day """ + async with Database('game.json', {'user':'~', 'bids':[]}) as state: + async with Database('players.json') as players: + if 'reup' not in state: + state['reup'] = 1 + if state['user'] != message.author.id: + await self.send_message( + message.channel, + "You are not currently playing a game" + ) + elif 'played' in state and not state['played']: + await self.send_message( + message.channel, + "You should play your game first" + ) + elif players[state['user']]['balance'] < state['reup']: + await self.send_message( + message.channel, + "You do not have enough tokens to extend this session" + ) + else: + state['time'] = time.time() - ( + 86400 + max( + 0, + (state['time'] + 172800) - time.time() + ) + ) + # 1 day + the remaining time + players[state['user']]['balance'] -= state['reup'] + state['reup'] += 1 + await self.send_message( + self.fetch_channel('story'), + "The current game session has been extended" + ) @bot.subscribe('startgame') async def start_game(self, evt): @@ -428,7 +467,7 @@ async def start_game(self, evt): 'balance':10 } user = self.fetch_channel('story').server.get_member(bid['user']) - if bid['amount'] < players[bid['user']]['balance']: + if bid['amount'] > players[bid['user']]['balance']: await self.send_message( user, "You do not have enough tokens to cover your" @@ -446,7 +485,7 @@ async def start_game(self, evt): state['played'] = False state['refund'] = max(0, bid['amount'] - 1) state['time'] = time.time() - state['bids'] = [{'user':'', 'amount':1, 'game':''}] + state['bids'] = [{'user':'', 'amount':0, 'game':''}] state.save() self.player = Player(bid['game']) # in future: @@ -474,7 +513,7 @@ async def start_game(self, evt): '%s is now playing %s\n' 'The game will begin shortly' % ( user.mention, - content[1] + bid['game'] ) ) # Post to general @@ -484,6 +523,12 @@ async def start_game(self, evt): '```'+self.player.readchunk()+'```' ) return + state['user'] = '~' + state['transcript'] = [] + state['game'] = '' + state['reup'] = 1 + state['bids'] = [{'user':'', 'amount':0, 'game':''}] + state.save() await self.send_message( self.fetch_channel('story'), "None of the bidders for the current game session could" @@ -491,6 +536,7 @@ async def start_game(self, evt): " awarded to the first bidder" ) + @bot.subscribe('command') async def record_command(self, evt, command, user): async with Database('weekly.json') as week: @@ -573,4 +619,71 @@ async def reset_week(self): payout ) + @bot.add_task(1800) # 30 minutes + async def check_game(self): + async with Database('game.json', {'user':'~', 'bids':[]}) as state: + now = time.time() + if state['user'] != '~' and now - state['time'] >= 172800: # 2 days + async with Database('players.json') as players: + user = self.fetch_channel('story').server.get_member(state['user']) + if 'played' in state and not state['played']: + await self.send_message( + user, + "Your game has ended without being played. " + "You are being refunded %d tokens" % ( + state['refund'] + ) + ) + players[state['user']]['balance'] += state['refund'] + else: + self.player.write('score') + self.player.readchunk() + self.player.quit() + await self.send_message( + user, + 'Your game has ended. Your score was %d\n' + 'Thanks for playing! You will receive %d tokens' % ( + self.player.score, + self.player.score + ) + ) + players[state['user']]['balance'] += self.player.score + # print("Granting xp for score payout") + self.dispatch( + 'grant_xp', + user, + self.player.score * 10 #maybe normalize this since each game scores differently + ) + state['user'] = '~' + del state['transcript'] + state['user'] = '~' + del self.player + state.save() + if 'bids' not in state or len(state['bids']) == 1: + await self.send_message( + self.fetch_channel('story'), + "The game is now idle and will be awarded to the first bidder" + ) + else: + self.dispatch('startgame') + elif state['user'] != '~' and now - state['time'] >= 151200: # 6 hours left + await self.send_message( + self.fetch_channel('story').server.get_member(state['user']), + "Your current game of %s is about to expire. If you wish to extend" + " your game session, you can `!reup` at a cost of %d tokens," + " which will grant you an additional day" % ( + state['game'], + state['reup'] + ) + ) + elif ('played' not in state or state['played']) and state['user'] != '~' and now - state['time'] >= 86400: # 1 day left + await self.send_message( + self.fetch_channel('story').server.get_member(state['user']), + "Your current game of %s will expire in less than 1 day. If you" + " wish to extend your game session, you can `!reup` at a cost of" + " %d tokens, which will grant you an additional day" % ( + state['game'], + state['reup'] + ) + ) return bot From df75fe39a65022bb93bd6d909ad299d24d7d17bf Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 21 Feb 2018 15:39:55 -0500 Subject: [PATCH 17/23] Added quoting --- bots/core.py | 6 +++--- bots/story.py | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bots/core.py b/bots/core.py index c970aac..ee7f15a 100644 --- a/bots/core.py +++ b/bots/core.py @@ -305,7 +305,7 @@ async def shutdown(self): await asyncio.wait(tasks) await self.close() - async def send_message(self, destination, content, *, delim='\n', **kwargs): + async def send_message(self, destination, content, *, delim='\n', quote='', **kwargs): #built in chunking body = content.split(delim) tmp = [] @@ -336,7 +336,7 @@ async def send_message(self, destination, content, *, delim='\n', **kwargs): # 1KB chunking target last_msg = await super().send_message( destination, - msg, + quote+msg+quote, **kwargs ) tmp = [] @@ -345,7 +345,7 @@ async def send_message(self, destination, content, *, delim='\n', **kwargs): #send any leftovers (guaranteed <2KB) last_msg = await super().send_message( destination, - msg + quote+msg+quote ) return last_msg diff --git a/bots/story.py b/bots/story.py index 3d660dc..712f10f 100644 --- a/bots/story.py +++ b/bots/story.py @@ -164,7 +164,8 @@ async def state_router(self, message, content): self.player.write('\n') await self.send_message( message.channel, - '```'+self.player.readchunk()+'```' + self.player.readchunk(), + quote='```' ) elif content == 'score': self.player.write('score') @@ -222,7 +223,8 @@ async def state_router(self, message, content): self.player.write(content) await self.send_message( message.channel, - '```'+self.player.readchunk()+'```' + self.player.readchunk(), + quote='```' ) else: await self.send_message( @@ -520,7 +522,8 @@ async def start_game(self, evt): await asyncio.sleep(2) await self.send_message( self.fetch_channel('story'), - '```'+self.player.readchunk()+'```' + self.player.readchunk(), + quote='```' ) return state['user'] = '~' From f5a2e0d1ca319ffb2f70ee12817641ed88ebb57c Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Fri, 23 Feb 2018 15:56:32 +0000 Subject: [PATCH 18/23] Added simple exception handling when failing to deliver a message --- bots/core.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/bots/core.py b/bots/core.py index ee7f15a..696dd2d 100644 --- a/bots/core.py +++ b/bots/core.py @@ -334,19 +334,33 @@ async def send_message(self, destination, content, *, delim='\n', quote='', **kw elif len(msg) > 1024: # Otherwise, send it if the current message has reached the # 1KB chunking target - last_msg = await super().send_message( - destination, - quote+msg+quote, - **kwargs - ) + try: + last_msg = await super().send_message( + destination, + quote+msg+quote, + **kwargs + ) + except discord.errors.HTTPException as e: + print("Failed to deliver message:", e.text) + await super().send_message( + self.fetch_channel('dev'), + "Failed to deliver a message to "+str(destination) + ) tmp = [] await asyncio.sleep(1) if len(tmp): #send any leftovers (guaranteed <2KB) - last_msg = await super().send_message( - destination, - quote+msg+quote - ) + try: + last_msg = await super().send_message( + destination, + quote+msg+quote + ) + except discord.errors.HTTPException as e: + print("Failed to deliver message:", e.text) + await super().send_message( + self.fetch_channel('dev'), + "Failed to deliver a message to "+str(destination) + ) return last_msg def getid(self, username): From 3aeecee42f0b01a36402c8dc6f87658a919be9d7 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Fri, 23 Feb 2018 17:39:01 +0000 Subject: [PATCH 19/23] Added highscore and timeleft Also prevented current player from bidding --- bots/story.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 8 deletions(-) diff --git a/bots/story.py b/bots/story.py index 712f10f..2a7d8bd 100644 --- a/bots/story.py +++ b/bots/story.py @@ -8,6 +8,7 @@ import threading import time import re +from math import ceil more_patterns = [ re.compile(r'\*+(MORE|more)\*+') @@ -111,6 +112,8 @@ def quit(self): os.close(self.stdoutRead) os.close(self.stdoutWrite) +def avg(n): + return sum(n)/len(n) def EnableStory(bot): if not isinstance(bot, CoreBot): @@ -189,21 +192,45 @@ async def state_router(self, message, content): self.player.write('score') self.player.readchunk() self.player.quit() + async with Database('scores.json') as scores: + if state['game'] not in scores: + scores[state['game']] = [] + scores[state['game']].append([ + self.player.score, + state['user'] + ]) + scores.save() + modifier = avg( + [score[0] for game in scores for score in scores[game]] + ) / max(1, avg( + [score[0] for score in scores[state['game']]] + )) + norm_score = ceil(self.player.score * modifier) + if self.player.score > 0: + norm_score = max(norm_score, 1) await self.send_message( message.channel, - 'You have quit your game. Your score was %d\n' + 'Your game has ended. Your score was %d\n' 'Thanks for playing! You will receive %d tokens' % ( self.player.score, - self.player.score + norm_score ) ) - players[message.author.id]['balance'] += self.player.score - state['user'] = '~' + if self.player.score > max([score[0] for score in scores[state['game']]]): + await self.send_message( + self.fetch_channel('story'), + "%s has just set the high score on %s at %d points" % ( + message.author.mention, + state['game'], + self.player.score + ) + ) + players[state['user']]['balance'] += norm_score # print("Granting xp for score payout") self.dispatch( 'grant_xp', message.author, - self.player.score * 10 #maybe normalize this since each game scores differently + norm_score * 10 #maybe normalize this since each game scores differently ) del state['transcript'] state['user'] = '~' @@ -332,6 +359,13 @@ async def cmd_bid(self, message, content): Example: `!bid 1 zork1` """ async with Database('game.json', {'user':'~'}) as state: + if message.author.id == state['user']: + await self.send_message( + message.channel, + "You can't place a bid while you're already playing a game." + " Why not give someone else a turn?" + ) + return async with Database('players.json') as players: bid = content[1] try: @@ -582,6 +616,72 @@ async def save_activity(self, evt): # print(week) week.save() + @bot.add_command('!timeleft') + async def cmd_timeleft(self, message, content): + """ + `!timeleft` : Gets the remaining time for the current game + """ + async with Database('game.json', {'user':'~', 'bids':[]}) as state: + if state['user'] == '~': + await self.send_message( + message.channel, + "Currently, nobody is playing a game" + ) + else: + delta = (state['time'] + 172800) - time.time() + d_days = delta // 86400 + delta = delta % 86400 + d_hours = delta // 3600 + delta = delta % 3600 + d_minutes = delta // 60 + d_seconds = delta % 60 + await self.send_message( + message.channel, + "%s's game of %s will end in %d days, %d hours, %d minutes, " + "and %d seconds" % ( + self.users[state['user']]['fullname'], + state['game'], + d_days, + d_hours, + d_minutes, + d_seconds + ) + ) + + @bot.add_command('!highscore') + async def cmd_highscore(self, message, content): + """ + `!highscore ` : Gets the current highscore for that game + Example: `!highscore zork1` + """ + if len(content) < 2: + await self.send_message( + message.channel, + "Please provide a game name with this command" + ) + return + async with Database('scores.json') as scores: + if content[1] in scores: + score, uid = sorted( + scores[content[1]], + key=lambda x:x[0], + reverse=True + )[0] + await self.send_message( + message.channel, + "High score for %s: %d set by %s" % ( + content[1], + score, + self.users[uid]['mention'] + ) + ) + else: + await self.send_message( + message.channel, + "No scores for this game yet" + ) + + @bot.add_task(604800) # 1 week async def reset_week(self): #{uid: {}} @@ -642,20 +742,45 @@ async def check_game(self): self.player.write('score') self.player.readchunk() self.player.quit() + async with Database('scores.json') as scores: + if state['game'] not in scores: + scores[state['game']] = [] + scores[state['game']].append([ + self.player.score, + state['user'] + ]) + scores.save() + modifier = avg( + [score[0] for game in scores for score in scores[game]] + ) / max(1, avg( + [score[0] for score in scores[state['game']]] + )) + norm_score = ceil(self.player.score * modifier) + if self.player.score > 0: + norm_score = max(norm_score, 1) await self.send_message( user, 'Your game has ended. Your score was %d\n' 'Thanks for playing! You will receive %d tokens' % ( self.player.score, - self.player.score + norm_score ) ) - players[state['user']]['balance'] += self.player.score + if self.player.score > max([score[0] for score in scores[state['game']]]): + await self.send_message( + self.fetch_channel('story'), + "%s has just set the high score on %s at %d points" % ( + self.users[state['user']]['mention'], + state['game'], + self.player.score + ) + ) + players[state['user']]['balance'] += norm_score # print("Granting xp for score payout") self.dispatch( 'grant_xp', user, - self.player.score * 10 #maybe normalize this since each game scores differently + norm_score * 10 #maybe normalize this since each game scores differently ) state['user'] = '~' del state['transcript'] From a49d7d6ba6c980a25fb75b4c63ae3bb9829cafc8 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Fri, 23 Feb 2018 17:45:37 +0000 Subject: [PATCH 20/23] Reup now simply adds a day instead of being dumb --- bots/story.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bots/story.py b/bots/story.py index 2a7d8bd..3c03649 100644 --- a/bots/story.py +++ b/bots/story.py @@ -475,12 +475,7 @@ async def cmd_reup(self, message, content): "You do not have enough tokens to extend this session" ) else: - state['time'] = time.time() - ( - 86400 + max( - 0, - (state['time'] + 172800) - time.time() - ) - ) + state['time'] += 86400 # 1 day + the remaining time players[state['user']]['balance'] -= state['reup'] state['reup'] += 1 From 1b78b4f04e3ff1e007e1b1887a3a63337a8c7190 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Tue, 27 Feb 2018 15:38:16 +0000 Subject: [PATCH 21/23] Various fixes Added toggle-comments command (comments now allowed by default) Removed amperstand and parethesis from sanitization character list in channel names Special handlers are now checked in the order they are added Added some more cleaning patterns to the game interface Moved game end logic to an event Fixed the reup command --- bots/core.py | 6 +- bots/help.py | 2 +- bots/party.py | 2 +- bots/story.py | 376 +++++++++++++++++++++++++++----------------------- 4 files changed, 206 insertions(+), 180 deletions(-) diff --git a/bots/core.py b/bots/core.py index 696dd2d..48fd790 100644 --- a/bots/core.py +++ b/bots/core.py @@ -19,6 +19,7 @@ class CoreBot(discord.Client): users = {} # id/fullname -> {id, fullname, mention, name} tasks = {} # taskname (auto generated) -> [interval(s), qualname] functions take (self) special = {} # eventname -> checker. callable takes (self, message) and returns True if function should be run. Func takes (self, message, content) + special_order = [] def add_command(self, *cmds): #decorator. Attaches the decorated function to the given command(s) if not len(cmds): @@ -76,6 +77,7 @@ def wrapper(func): if event in self.special: raise NameError("This special event already exists! Change the name of the special function") self.special[event] = check + self.special_order.append(event) @self.subscribe(event) async def run_special(self, evt, message, content): @@ -445,8 +447,8 @@ async def on_message(self, message): else: # If this was not a command, check if any of the special functions # would like to run on this message - for event, check in self.special.items(): - if check(self, message): + for event in self.special_order: + if self.special[event](self, message): print("Running special", event) self.dispatch(event, message, content) break diff --git a/bots/help.py b/bots/help.py index ce3bf6f..edbce23 100644 --- a/bots/help.py +++ b/bots/help.py @@ -1,5 +1,5 @@ from .core import CoreBot -from .utils import sanitize, Database +from .utils import sanitize, ListDatabase, Database import discord import asyncio import sys diff --git a/bots/party.py b/bots/party.py index 0984a3a..195ef77 100644 --- a/bots/party.py +++ b/bots/party.py @@ -6,7 +6,7 @@ import time def sanitize_channel(name): - return sanitize(name, '~!@#$%^&*()-', '_').rstrip() + return sanitize(name, '~!@#$%^*-', '_').rstrip() def EnableParties(bot): diff --git a/bots/story.py b/bots/story.py index 3c03649..ee4d5ad 100644 --- a/bots/story.py +++ b/bots/story.py @@ -10,6 +10,9 @@ import re from math import ceil +class GameEnded(OSError): + pass + more_patterns = [ re.compile(r'\*+(MORE|more)\*+') ] @@ -26,7 +29,8 @@ re.compile(r'Moves:[ ]*[0-9]+'), re.compile(r'Turns:[ ]*[0-9]+'), # re.compile(r'[0-9]+:[0-9]+ [AaPp][Mm]'), - re.compile(r' [0-9]+ \.') + re.compile(r' [0-9]+ \.'), + re.compile(r'^([>.][>.\s]*)') ] + more_patterns + score_patterns def multimatch(text, patterns): @@ -70,12 +74,20 @@ def readline(self): intake = self.remainder while b'\n' not in intake: intake += os.read(self.stdoutRead, 64) + print("Buffered intake:", intake) lines = intake.split(b'\n') self.remainder = b'\n'.join(lines[1:]) return lines[0].decode().rstrip() - def readchunk(self, clean=True): - content = [self.buffer.get()] + def readchunk(self, clean=True, timeout=None): + if timeout is not None: + print("The timeout parameter is deprecated") + if self.proc.returncode is not None: + raise GameEnded() + try: + content = [self.buffer.get(timeout=10)] + except queue.Empty: + raise GameEnded() try: while not self.buffer.empty(): content.append(self.buffer.get(timeout=0.5)) @@ -147,121 +159,110 @@ async def state_router(self, message, content): # Routes messages depending on the game state async with Database('game.json', {'user':'~'}) as state: if state['user'] == message.author.id: - if not hasattr(self, 'player'): - # The game has been interrupted - await self.send_message( - message.channel, - "Resuming game in progress...\n" - "Please wait" - ) - self.player = Player(state['game']) - for msg in state['transcript']: - self.player.write(msg) - await asyncio.sleep(0.5) - self.player.readchunk() - content = message.content.strip().lower() - if content == '$': - content = '\n' - state['transcript'].append(content) - state.save() - self.player.write('\n') - await self.send_message( - message.channel, - self.player.readchunk(), - quote='```' - ) - elif content == 'score': - self.player.write('score') - self.player.readchunk() - await self.send_message( - message.channel, - 'Your score is %d' % self.player.score - ) - elif content == 'quit': - async with Database('players.json') as players: - if 'played' in state and not state['played']: + try: + if not hasattr(self, 'player'): + # The game has been interrupted + await self.send_message( + message.channel, + "Resuming game in progress...\n" + "Please wait" + ) + self.player = Player(state['game']) + for msg in state['transcript']: + self.player.write(msg) + await asyncio.sleep(0.5) + self.player.readchunk() + if self.player.proc.returncode is not None: + await self.send_message( + message.channel, + "The game has ended" + ) + self.dispatch('endgame', message.author, message.channel) + content = message.content.strip().lower() + if content == '$': + state['transcript'].append('\n') + state.save() + self.player.write('\n') + await self.send_message( + message.channel, + self.player.readchunk(), + quote='```' + ) + if self.player.proc.returncode is not None: await self.send_message( message.channel, - "You quit your game without playing. " - "You are being refunded %d tokens" % ( - state['refund'] - ) + "The game has ended" ) - players[message.author.id]['balance'] += state['refund'] - else: - self.player.write('score') - self.player.readchunk() - self.player.quit() - async with Database('scores.json') as scores: - if state['game'] not in scores: - scores[state['game']] = [] - scores[state['game']].append([ - self.player.score, - state['user'] - ]) - scores.save() - modifier = avg( - [score[0] for game in scores for score in scores[game]] - ) / max(1, avg( - [score[0] for score in scores[state['game']]] - )) - norm_score = ceil(self.player.score * modifier) - if self.player.score > 0: - norm_score = max(norm_score, 1) + self.dispatch('endgame', message.author, message.channel) + elif content == 'score': + self.player.write('score') + self.player.readchunk() + await self.send_message( + message.channel, + 'Your score is %d' % self.player.score + ) + if self.player.proc.returncode is not None: await self.send_message( message.channel, - 'Your game has ended. Your score was %d\n' - 'Thanks for playing! You will receive %d tokens' % ( - self.player.score, - norm_score - ) - ) - if self.player.score > max([score[0] for score in scores[state['game']]]): - await self.send_message( - self.fetch_channel('story'), - "%s has just set the high score on %s at %d points" % ( - message.author.mention, - state['game'], - self.player.score - ) - ) - players[state['user']]['balance'] += norm_score - # print("Granting xp for score payout") - self.dispatch( - 'grant_xp', - message.author, - norm_score * 10 #maybe normalize this since each game scores differently + "The game has ended" ) - del state['transcript'] - state['user'] = '~' - del self.player - state.save() - if 'bids' not in state or len(state['bids']) == 1: + self.dispatch('endgame', message.author, message.channel) + elif content == 'quit': + self.dispatch('endgame', message.author, message.channel) + else: + state['played'] = True + state['transcript'].append(content) + state.save() + self.player.write(content) await self.send_message( - self.fetch_channel('story'), - "The game is now idle and will be awarded to the first bidder" + message.channel, + self.player.readchunk(), + quote='```' ) - else: - self.dispatch('startgame') - else: - state['played'] = True - state['transcript'].append(content) - state.save() - self.player.write(content) + if self.player.proc.returncode is not None: + await self.send_message( + message.channel, + "The game has ended" + ) + self.dispatch('endgame', message.author, message.channel) + except GameEnded: await self.send_message( message.channel, - self.player.readchunk(), - quote='```' + "It looks like this game has ended!" ) - else: + self.dispatch('endgame', message.author, message.channel) + elif 'restrict' in state and state['restrict']: await self.send_message( message.author, - "Please refrain from posting messages in the story channel" - " while someone else is playing" + "The current player has disabled comments in the story channel" ) await asyncio.sleep(0.5) await self.delete_message(message) + @bot.add_command('!toggle-comments') + async def cmd_toggle_comments(self, message, content): + """ + `!toggle-comments` : Toggles allowing spectator comments in the story_channel + """ + async with Database('game.json', {'user':'~'}) as state: + if state['user'] != message.author.id: + await self.send_message( + message.channel, + "You can't toggle comments if you're not playing" + ) + else: + if 'restrict' not in state: + state['restrict'] = True + else: + state['restrict'] = not state['restrict'] + await self.send_message( + self.fetch_channel('story'), + "Comments from spectators are now %s" % ( + 'forbidden' if state['restrict'] else 'allowed' + ) + ) + state.save() + @bot.add_command('!_start') async def cmd_start(self, message, content): """ @@ -423,11 +424,6 @@ async def cmd_bid(self, message, content): " are available, use `!games`" ) return - await self.send_message( - message.channel, - "Your bid has been placed. If you are not outbid, your" - " game will begin after the current game has ended" - ) user = self.fetch_channel('story').server.get_member(state['bids'][-1]['user']) if user: await self.send_message( @@ -449,6 +445,12 @@ async def cmd_bid(self, message, content): state.save() if state['user'] == '~': self.dispatch('startgame') + else: + await self.send_message( + message.channel, + "Your bid has been placed. If you are not outbid, your" + " game will begin after the current game has ended" + ) @bot.add_command('!reup') async def cmd_reup(self, message, content): @@ -483,6 +485,81 @@ async def cmd_reup(self, message, content): self.fetch_channel('story'), "The current game session has been extended" ) + state.save() + + @bot.subscribe('endgame') + async def end_game(self, evt, user, dest): + async with Database('game.json', {'user':'~'}) as state: + async with Database('players.json') as players: + if 'played' in state and not state['played']: + await self.send_message( + dest, + "You quit your game without playing. " + "You are being refunded %d tokens" % ( + state['refund'] + ) + ) + players[user.id]['balance'] += state['refund'] + else: + try: + self.player.write('score') + self.player.readchunk() + except GameEnded: + pass + finally: + self.player.quit() + async with Database('scores.json') as scores: + if state['game'] not in scores: + scores[state['game']] = [] + scores[state['game']].append([ + self.player.score, + state['user'] + ]) + scores.save() + modifier = avg( + [score[0] for game in scores for score in scores[game]] + ) / max(1, avg( + [score[0] for score in scores[state['game']]] + )) + norm_score = ceil(self.player.score * modifier) + if self.player.score > 0: + norm_score = max(norm_score, 1) + await self.send_message( + dest, + 'Your game has ended. Your score was %d\n' + 'Thanks for playing! You will receive %d tokens' % ( + self.player.score, + norm_score + ) + ) + if self.player.score > max([score[0] for score in scores[state['game']]]): + await self.send_message( + self.fetch_channel('story'), + "%s has just set the high score on %s at %d points" % ( + user.mention, + state['game'], + self.player.score + ) + ) + players[state['user']]['balance'] += norm_score + # print("Granting xp for score payout") + self.dispatch( + 'grant_xp', + user, + norm_score * 10 #maybe normalize this since each game scores differently + ) + del state['transcript'] + state['user'] = '~' + del self.player + state.save() + players.save() + if 'bids' not in state or len(state['bids']) == 1: + await self.send_message( + self.fetch_channel('story'), + "The game is now idle and will be awarded to the first bidder" + ) + else: + self.dispatch('startgame') @bot.subscribe('startgame') async def start_game(self, evt): @@ -512,6 +589,7 @@ async def start_game(self, evt): players.save() state['user'] = bid['user'] state['transcript'] = [] + state['restrict'] = False state['game'] = bid['game'] state['played'] = False state['refund'] = max(0, bid['amount'] - 1) @@ -530,14 +608,25 @@ async def start_game(self, evt): 'Any message you type in the story channel will be interpreted' ' as input to the game **unless** your message starts with `!`' ' (my commands)\n' + '`!reup` : Use this command to add a day to your game session\n' + 'This costs 1 token, and the cost will increase each time\n' + '`!toggle-comments` : Use this command to toggle permissions in the story channel\n' + 'Right now, anyone can send messages in the story channel' + ' while you\'re playing. If you use `!toggle-comments`,' + ' nobody but you will be allowed to send messages.\n' '`$` : Simply type `$` to enter a blank line to the game\n' 'That can be useful if the game is stuck or ' 'if it ignored your last input\n' + 'Some menus may ask you to type a space to continue.\n' '`quit` : Quits the game in progress\n' 'This is also how you end the game if you finish it\n' '`score` : View your score\n' 'Some games may have their own commands in addition to these' - ' ones that I handle personally' + ' ones that I handle personally\n' + 'Lastly, if you want to make a comment in the channel' + ' without me forwarding your message to the game, ' + 'simply start the message with `! `, for example:' + ' `! Any ideas on how to unlock this door?`' ) await self.send_message( self.fetch_channel('story'), @@ -722,73 +811,8 @@ async def check_game(self): async with Database('game.json', {'user':'~', 'bids':[]}) as state: now = time.time() if state['user'] != '~' and now - state['time'] >= 172800: # 2 days - async with Database('players.json') as players: - user = self.fetch_channel('story').server.get_member(state['user']) - if 'played' in state and not state['played']: - await self.send_message( - user, - "Your game has ended without being played. " - "You are being refunded %d tokens" % ( - state['refund'] - ) - ) - players[state['user']]['balance'] += state['refund'] - else: - self.player.write('score') - self.player.readchunk() - self.player.quit() - async with Database('scores.json') as scores: - if state['game'] not in scores: - scores[state['game']] = [] - scores[state['game']].append([ - self.player.score, - state['user'] - ]) - scores.save() - modifier = avg( - [score[0] for game in scores for score in scores[game]] - ) / max(1, avg( - [score[0] for score in scores[state['game']]] - )) - norm_score = ceil(self.player.score * modifier) - if self.player.score > 0: - norm_score = max(norm_score, 1) - await self.send_message( - user, - 'Your game has ended. Your score was %d\n' - 'Thanks for playing! You will receive %d tokens' % ( - self.player.score, - norm_score - ) - ) - if self.player.score > max([score[0] for score in scores[state['game']]]): - await self.send_message( - self.fetch_channel('story'), - "%s has just set the high score on %s at %d points" % ( - self.users[state['user']]['mention'], - state['game'], - self.player.score - ) - ) - players[state['user']]['balance'] += norm_score - # print("Granting xp for score payout") - self.dispatch( - 'grant_xp', - user, - norm_score * 10 #maybe normalize this since each game scores differently - ) - state['user'] = '~' - del state['transcript'] - state['user'] = '~' - del self.player - state.save() - if 'bids' not in state or len(state['bids']) == 1: - await self.send_message( - self.fetch_channel('story'), - "The game is now idle and will be awarded to the first bidder" - ) - else: - self.dispatch('startgame') + user = self.fetch_channel('story').server.get_member(state['user']) + self.dispatch('endgame', user, user) elif state['user'] != '~' and now - state['time'] >= 151200: # 6 hours left await self.send_message( self.fetch_channel('story').server.get_member(state['user']), @@ -796,7 +820,7 @@ async def check_game(self): " your game session, you can `!reup` at a cost of %d tokens," " which will grant you an additional day" % ( state['game'], - state['reup'] + state['reup'] if 'reup' in state else 1 ) ) elif ('played' not in state or state['played']) and state['user'] != '~' and now - state['time'] >= 86400: # 1 day left @@ -806,7 +830,7 @@ async def check_game(self): " wish to extend your game session, you can `!reup` at a cost of" " %d tokens, which will grant you an additional day" % ( state['game'], - state['reup'] + state['reup'] if 'reup' in state else 1 ) ) return bot From b2ce3788ffa0e202dd6fca8809ed808231b599b3 Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Tue, 27 Feb 2018 17:26:40 +0000 Subject: [PATCH 22/23] Fixed more recognition in player Updated score normalization to reward long games --- bots/story.py | 109 +++++++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/bots/story.py b/bots/story.py index ee4d5ad..37d18f1 100644 --- a/bots/story.py +++ b/bots/story.py @@ -8,7 +8,7 @@ import threading import time import re -from math import ceil +from math import ceil, floor class GameEnded(OSError): pass @@ -18,7 +18,7 @@ class GameEnded(OSError): ] score_patterns = [ - re.compile(r'([0-9]+)/[0-9+]'), + re.compile(r'([0-9]+)/[0-9]+'), re.compile(r'Score:[ ]*([-]*[0-9]+)'), re.compile(r'([0-9]+):[0-9]+ [AaPp][Mm]') ] @@ -30,7 +30,8 @@ class GameEnded(OSError): re.compile(r'Turns:[ ]*[0-9]+'), # re.compile(r'[0-9]+:[0-9]+ [AaPp][Mm]'), re.compile(r' [0-9]+ \.'), - re.compile(r'^([>.][>.\s]*)') + re.compile(r'^([>.][>.\s]*)'), + re.compile(r'Warning: @[\w_]+ called .*? \(PC = \w+\) \(will ignore further occurrences\)') ] + more_patterns + score_patterns def multimatch(text, patterns): @@ -71,13 +72,14 @@ def reader(self): self.buffer.put(self.readline()) def readline(self): - intake = self.remainder - while b'\n' not in intake: - intake += os.read(self.stdoutRead, 64) - print("Buffered intake:", intake) - lines = intake.split(b'\n') - self.remainder = b'\n'.join(lines[1:]) - return lines[0].decode().rstrip() + # intake = self.remainder + # while b'\n' not in intake: + # intake += os.read(self.stdoutRead, 64) + # print("Buffered intake:", intake) + # lines = intake.split(b'\n') + # self.remainder = b'\n'.join(lines[1:]) + # return lines[0].decode().rstrip() + return os.read(self.stdoutRead, 256).decode() def readchunk(self, clean=True, timeout=None): if timeout is not None: @@ -94,22 +96,32 @@ def readchunk(self, clean=True, timeout=None): except queue.Empty: pass + #now merge up lines + # print("Raw content:", ''.join(content)) + # import pdb; pdb.set_trace() + content = [line.rstrip() for line in ''.join(content).split('\n')] + # clean metadata if multimatch(content[-1], more_patterns): self.write('\n') + time.sleep(0.25) content += self.readchunk(False) - if clean: - for i in range(len(content)): - line = content[i] - result = multimatch(line, score_patterns) - if result: - self.score = int(result.group(1)) + # print("Merged content:", content) + + if not clean: + return content + + for i in range(len(content)): + line = content[i] + result = multimatch(line, score_patterns) + if result: + self.score = int(result.group(1)) + result = multimatch(line, clean_patterns) + while result: + line = result.re.sub('', line) result = multimatch(line, clean_patterns) - while result: - line = result.re.sub('', line) - result = multimatch(line, clean_patterns) - content[i] = line + content[i] = line return '\n'.join(line for line in content if len(line.rstrip())) def quit(self): @@ -194,6 +206,12 @@ async def state_router(self, message, content): "The game has ended" ) self.dispatch('endgame', message.author, message.channel) + elif content == 'save': + await self.send_message( + message.channel, + "Unfortunately, saved games are not supported at " + "this time." + ) elif content == 'score': self.player.write('score') self.player.readchunk() @@ -521,9 +539,15 @@ async def end_game(self, evt, user, dest): ) / max(1, avg( [score[0] for score in scores[state['game']]] )) - norm_score = ceil(self.player.score * modifier) - if self.player.score > 0: - norm_score = max(norm_score, 1) + norm_score = ceil(self.player.score * modifier) + norm_score += floor( + len(state['transcript']) / 25 * min( + modifier, + 1 + ) + ) + if self.player.score > 0: + norm_score = max(norm_score, 1) await self.send_message( dest, 'Your game has ended. Your score was %d\n' @@ -541,13 +565,14 @@ async def end_game(self, evt, user, dest): self.player.score ) ) - players[state['user']]['balance'] += norm_score - # print("Granting xp for score payout") - self.dispatch( - 'grant_xp', - user, - norm_score * 10 #maybe normalize this since each game scores differently - ) + if norm_score > 0: + players[state['user']]['balance'] += norm_score + # print("Granting xp for score payout") + self.dispatch( + 'grant_xp', + user, + norm_score * 10 #maybe normalize this since each game scores differently + ) del state['transcript'] state['user'] = '~' del self.player @@ -680,6 +705,7 @@ async def record_command(self, evt, command, user): user, 5 ) + week[user.id]['active'] = True week.save() @bot.subscribe('after:message') @@ -693,9 +719,9 @@ async def save_activity(self, evt): # print(week, self._pending_activity) for uid in self._pending_activity: if uid not in week: - week[uid]={'active':'yes'} + week[uid]={'active':True} else: - week[uid]['active']='yes' + week[uid]['active']=True self._pending_activity = set() # print(week) week.save() @@ -775,8 +801,6 @@ async def reset_week(self): xp = [] for uid in week: user = self.fetch_channel('story').server.get_member(uid) #icky! - if 'active' in week[uid] or uid in self._pending_activity: - xp.append([user, 5]) if uid not in players: players[uid] = { 'level':1, @@ -787,14 +811,17 @@ async def reset_week(self): if players[user.id]['balance'] < 20*players[user.id]['level']: payout *= 2 players[uid]['balance'] += payout - await self.send_message( - self.fetch_channel('story').server.get_member(uid), #icky! - "Your allowance was %d tokens this week. Your balance is now %d " - "tokens" % ( - payout, - players[uid]['balance'] + if 'active' in week[uid] or uid in self._pending_activity: + xp.append([user, 5]) + #only notify if they were active. Otherwise don't bother them + await self.send_message( + self.fetch_channel('story').server.get_member(uid), #icky! + "Your allowance was %d tokens this week. Your balance is now %d " + "tokens" % ( + payout, + players[uid]['balance'] + ) ) - ) self._pending_activity = set() players.save() os.remove('weekly.json') From 6068fce17992e2039844456ac7841d79baca0cab Mon Sep 17 00:00:00 2001 From: Aaron Graubert Date: Wed, 28 Feb 2018 00:05:23 +0000 Subject: [PATCH 23/23] Added dfrotz and games to install.md --- INSTALL.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 4f1fd14..a1e0103 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -157,7 +157,18 @@ don't know what a shell is, you're going to have a bad time rule), the fallback behavior is to allow the command if it is not an **Underscore Command**. This is the lowest priority behavior, so it can be overridden by any rule, including `defaults`. -4. Connect your bot to your **Server** +4. Get extras + 1. If you plan on using the story/text game system, you'll need to get a few things first: + * dfrotz (put `dfrotz` in the same directory as `main.py`): + ```bash + $ git clone https://github.com/DavidGriffith/frotz.git + $ cd frotz + $ make dumb + ``` + * Some `.z5` games (put them in a folder called `games` in the same directory + as `main.py`) + * You can get a good starter pack from [textplayer](https://github.com/danielricks/textplayer) +5. Connect your bot to your **Server** 1. At this point, your bot is configured and ready to work. Go back to the [Discord Developers page](https://discordapp.com/developers/) from step 2.1 and navigate to your bot