Skip to content

Commit

Permalink
Merge pull request #26 from luke-hdl/3-retest-system
Browse files Browse the repository at this point in the history
Basic Retest System
  • Loading branch information
luke-hdl committed Apr 17, 2024
2 parents 4e2465a + f38100c commit e9af538
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 15 deletions.
28 changes: 28 additions & 0 deletions src/run/challenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __init__(self, channel_for_challenge, aggressor, defenders):
self.time_of_issue = time.time()
self.responses = {aggressor: Response(aggressor, Role.AGGRESSOR)}
self.aggressor_response = self.responses[aggressor]
self.total_rounds = 0
for defender in defenders:
self.responses[defender] = Response(defender, Role.DEFENDER)

Expand All @@ -29,6 +30,12 @@ def is_complete(self):
return False
return True

def everyone_declined_retests(self):
for response in self.responses.values():
if not response.declined_to_retest:
return False
return True

def did_everyone_bid(self):
for response in self.responses.values():
if response.bid is None:
Expand Down Expand Up @@ -84,6 +91,8 @@ def __init__(self, responder, role):
self.complete = False
self.response = None
self.bid = None
self.declined_to_retest = False
self.retests = {} #Map of strings to the number of times they've been used.

def get_response_description(self, include_bid_information, aggressor_response):
response = "{} ({}) threw: {}".format(self.responder.mention, self.role.get_as_readable(), self.response)
Expand All @@ -99,11 +108,29 @@ def get_response_description(self, include_bid_information, aggressor_response):
response += " and tied, bidding more than the aggressor"
response += "\r\n"
return response

def set_response(self, player_response):
if self.response is not None:
raise ResponderHasAlreadyRespondedException
self.response = player_response

def decline_retest(self):
self.declined_to_retest = True

def reset_retest_status(self):
self.declined_to_retest = False
self.complete = False

def begin_retest(self):
self.response = None
self.bid = None
self.complete = False

def log_retest(self, retest):
if retest not in self.retests:
self.retests[retest] = 0
self.retests[retest] += 1

def get_response_status(self):
if self.complete:
return ResponseStatus.COMPLETE
Expand Down Expand Up @@ -141,3 +168,4 @@ class ResponseStatus(Enum):
WAITING_FOR_RESPONSE = 1
WAITING_FOR_BID = 2
COMPLETE = 3
WAITING_ON_RETEST = 4
110 changes: 104 additions & 6 deletions src/run/message_responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,51 @@ def clean_up_and_split_message_text(self, message_text):

return clean_split

def get_self_mention(self, ):
def get_self_mention(self):
return self.client.user.mention

async def process_guild_message_text_from_player(self, channel, message):
if self.client.user not in message.mentions:
return # By default, SlapChop shouldn't see messages that it doesn't ask for, but it's better safe than sorry.
message_text = message.content
tokenized_message_text = re.split("[ \t]{1,1000}", message_text)
if equal_inputs(tokenized_message_text[1], "retest"):
if message.author not in self.challenges_by_player:
await channel.send(
message.author.mention + ", you're not in a challenge!")
return
if self.challenges_by_player[message.author].channel != message.channel:
await channel.send(
message.author.mention + ", I can only understand retests in the channels the challenge was first posted in. Please send me another one.")
return
if self.challenges_by_player[message.author].responses[message.author].declined_to_retest:
await channel.send(
message.author.mention + ", you've already declined to retest. (Or you won the challenge outright, in which case you don't need to!)")
return
retest = "Not Specified"
if len(tokenized_message_text) >= 3:
retest = tokenized_message_text[2]
await channel.send("Your retest round has begun!")
await self.begin_retest(message.author, retest)
return

if equal_inputs(tokenized_message_text[1], "decline"):
if message.author not in self.challenges_by_player:
await channel.send(
message.author.mention + ", you're not in a channel!")
return
if self.challenges_by_player[message.author].channel != message.channel:
await channel.send(
message.author.mention + ", I can only understand retests (including declining them) in the channels the challenge was first posted in. Please send me another one.")
return
if self.challenges_by_player[message.author].responses[message.author].declined_to_retest:
await channel.send(
message.author.mention + ", you've already declined to retest. (Or you won the challenge outright, in which case you don't need to!)")
return
await channel.send(message.author.mention + ", I've noted you declined to retest.")
await self.decline_retest(message.author)
return

if equal_inputs(tokenized_message_text[1], "challenge"):
if message.author in self.challenges_by_player:
await channel.send(
Expand Down Expand Up @@ -185,6 +222,10 @@ async def process_direct_message_text_from_player(self, channel, player, message
"Your bid has been recorded! You'll receive a DM and a mention in the challenge channel when everyone has responded.")
case ResponseStatus.COMPLETE:
raise ResponderHasAlreadyRespondedException # lets us fold this case into the error handling.
case ResponseStatus.WAITING_ON_RETEST:
await channel.send(
"Hey there! I'm waiting to hear on players for whether they'd like to retest. You can only request a retest in the channel your challenge was made in, so that everyone can see it. If people aren't responding, and you'd like to leave the challenge, you can send me the word 'leave' to leave the challenge. (This cancels the challenge for everyone, though!)")

except ResponderNotInChallengeException:
await channel.send(
"Hey there! You're not in a challenge right now - you can only issue challenges in a channel that you, I, and whoever you're challenging all have access to. If you'd like more information, please send me the word 'help'.")
Expand All @@ -200,20 +241,77 @@ async def process_direct_message_text_from_player(self, channel, player, message

await self.process_challenge_status_for_player(player)

async def decline_retest(self, player):
challenge = self.challenges_by_player.get(player)
challenge.responses[player].decline_retest()
await self.process_challenge_status_for_player(player)

async def begin_retest(self, player, retest):
challenge = self.challenges_by_player.get(player)
challenge.total_rounds += 1
challenge.responses[player].log_retest(retest)
for player in challenge.responses.keys():
challenge.responses[player].begin_retest()
await player.send("A retest has begun for your challenge! Please submit your bid.")

async def process_challenge_status_for_player(self, player):
challenge = self.challenges_by_player.get(player)
if challenge is not None and (len(challenge.get_players_in_challenge()) > 2 or challenge.everyone_declined_retests()):
await self.finalize_challenge(challenge)
if challenge is not None and challenge.is_complete():
await self.complete_challenge(challenge)

async def complete_challenge(self, challenge):
await self.post_challenge_results(challenge)
players = list(challenge.get_players_in_challenge())
response_1 = challenge.responses[players[0]]
response_2 = challenge.responses[players[1]]
await players[0].send("This round of chops is done! Go to the channel to see results or rebid.")
await players[1].send("This round of chops is done! Go to the channel to see results or rebid.")
players_who_may_retest = []
if automatically_determine_winner(response_1.response, response_2.response) == RecognizedResult.PLAYER_1_WIN:
challenge.responses[players[0]].reset_retest_status()
challenge.responses[players[1]].reset_retest_status()
challenge.responses[players[0]].decline_retest() #Winner automatically denies.
players_who_may_retest.append(players[1])
elif automatically_determine_winner(response_1.response, response_2.response) == RecognizedResult.PLAYER_1_LOSS:
challenge.responses[players[0]].reset_retest_status()
challenge.responses[players[1]].reset_retest_status()
challenge.responses[players[1]].decline_retest() #Winner automatically denies.
players_who_may_retest.append(players[0])
else:
challenge.responses[players[0]].reset_retest_status()
challenge.responses[players[1]].reset_retest_status()
players_who_may_retest.append(players[0])
players_who_may_retest.append(players[1])

if challenge.total_rounds == 0: #More detailed guidance on the first retest round.
mentions = ""
for retesting_player in players_who_may_retest:
mentions += retesting_player.mention + " "
await challenge.channel.send(mentions + "- you may retest if you'd like by posting: \r\n\r\n " + self.client.user.mention + " retest ability\r\n\r\n now (replacing ability with the nature of the retest - like Brawl or Lucky.\r\nIf you don't want to retest, please instead post \r\n\r\n" + self.client.user.mention + " decline\r\n\r\n")
else:
mentions = "Players who can retest: "
for retesting_player in players_who_may_retest:
mentions += retesting_player.mention
retest_response = challenge.responses[retesting_player]
if len(retest_response.retests) == 0:
mentions += " (No retests yet.)"
else:
mentions += " (Used retests this challenge: "
for retest in retest_response.retests:
mentions += retest + " x" + str(retest_response.retests[retest]) + " "
mentions += ")"
mentions += " - "
await challenge.channel.send(mentions + "retest or decline now.")

async def finalize_challenge(self, challenge):
with self.alter_challenges_lock:
for notify_player in challenge.get_players_in_challenge():
self.challenges_by_player.pop(notify_player)
try:
await notify_player.send(
"Your challenge has been completed! See the channel it was posted in for results!")
"Your challenge has been completed, and no one has decided to retest! I'm posting a copy of the final challenge results for everyone.")
except:
pass

await self.post_challenge_results(challenge)
del challenge

Expand Down Expand Up @@ -281,7 +379,7 @@ async def on_ready(self, ):
print(f'We have logged in as {self.client.user}')

async def on_message(self, message):
if message.author == self.client.user:
if message.author.bot:
return
await self.expire_challenges()
if message.channel.guild is not None:
Expand Down
60 changes: 59 additions & 1 deletion src/run/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
def automatically_determine_winner(string1, string2):
chop1 = get_recognized_chop(string1)
chop2 = get_recognized_chop(string2)
if chop1 == RecognizedChops.UNKNOWN or chop2 == RecognizedChops.UNKNOWN:
return RecognizedResult.UNKNOWN
if chop1 == chop2:
return RecognizedResult.TIE
if chop1 == RecognizedChops.ROCK:
match chop2:
case RecognizedChops.SCISSORS:
return RecognizedResult.PLAYER_1_WIN
case __:
return RecognizedResult.PLAYER_1_LOSS
if chop1 == RecognizedChops.PAPER:
match chop2:
case RecognizedChops.ROCK:
return RecognizedResult.PLAYER_1_WIN
case __:
return RecognizedResult.PLAYER_1_LOSS
if chop1 == RecognizedChops.SCISSORS:
match chop2:
case RecognizedChops.ROCK:
return RecognizedResult.PLAYER_1_LOSS
case __:
return RecognizedResult.PLAYER_1_WIN
if chop1 == RecognizedChops.BOMB:
match chop2:
case RecognizedChops.SCISSORS:
return RecognizedResult.PLAYER_1_LOSS
case __:
return RecognizedResult.PLAYER_1_WIN
return RecognizedResult.UNKNOWN

def equal_inputs(string1, string2):
#SlapChop is case-insensitive.
#In the future, I'd like to add additional rules, e.g., ignoring internal spaces and text in parens, but not yet.
Expand All @@ -9,4 +42,29 @@ def equal_input_to_one_of_list(string1, list_of_strings):
for string2 in list_of_strings:
if equal_inputs(string1, string2):
return True
return False
return False

class RecognizedChops:
ROCK = 1
PAPER = 2
SCISSORS = 3
BOMB = 4
UNKNOWN = 5

class RecognizedResult:
PLAYER_1_WIN = 1
PLAYER_1_LOSS = 2
TIE = 3
UNKNOWN = 4

def get_recognized_chop(from_chop):
if equal_input_to_one_of_list(from_chop, ["rock", "r", ":new_moon_with_face", "rcok"]):
return RecognizedChops.ROCK
elif equal_input_to_one_of_list(from_chop,["paper", "p", ":scroll:", ":paper:"]):
return RecognizedChops.PAPER
elif equal_input_to_one_of_list(from_chop, ["scissors", "s", ":scissors:", "sissors", "scsissors"]):
return RecognizedChops.SCISSORS
elif equal_input_to_one_of_list(from_chop, ["bomb", "b", ":boom:", "bom"]):
return RecognizedChops.BOMB
else:
return RecognizedChops.UNKNOWN
2 changes: 2 additions & 0 deletions src/test/framework/spoofer_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def __init__(self, users, guild_channels, new_client):
class Client:
def __init__(self, user):
self.user = user
user.bot = True

class Message:
def __init__(self, author, channel, content, mentions):
Expand All @@ -36,6 +37,7 @@ def __init__(self, display_name):
self.display_name = display_name
self.mention = "<" + str(int(hash(display_name))) + ">"
player_mention_map[self.mention] = self
self.bot = False

async def send(self, message):
await self.channel.send(message)
Expand Down
34 changes: 34 additions & 0 deletions src/test/test_basic_input_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ async def test_input_list_equality():
expect_not(equal_input_to_one_of_list("vampire", []), "Empty list disinclusion not functional.")
expect_not(equal_input_to_one_of_list("vampire", [None, "werewolf"]), "List with None disinclusion not functional.")

@test
async def test_rps_logic():
expect(automatically_determine_winner("rock", "scissors") == RecognizedResult.PLAYER_1_WIN, "Rock should beat scissors.")
expect(automatically_determine_winner("rock", "paper") == RecognizedResult.PLAYER_1_LOSS, "Rock should lose to paper.")
expect(automatically_determine_winner("rock", "rock") == RecognizedResult.TIE, "Rock should tie itself.")
expect(automatically_determine_winner("rock", "bomb") == RecognizedResult.PLAYER_1_LOSS, "Rock should lose to bomb.")
expect(automatically_determine_winner("rock", "candy") == RecognizedResult.UNKNOWN, "Rock should have an unknown result against odd input.")

expect(automatically_determine_winner("paper", "scissors") == RecognizedResult.PLAYER_1_LOSS, "Paper should lose to scissors.")
expect(automatically_determine_winner("paper", "paper") == RecognizedResult.TIE, "Paper should tie paper.")
expect(automatically_determine_winner("paper", "rock") == RecognizedResult.PLAYER_1_WIN, "Paper should beat rock.")
expect(automatically_determine_winner("paper", "bomb") == RecognizedResult.PLAYER_1_LOSS, "Paper should lose to bomb.")
expect(automatically_determine_winner("paper", "candy") == RecognizedResult.UNKNOWN, "Paper should have an unknown result against odd input.")

expect(automatically_determine_winner("scissors", "scissors") == RecognizedResult.TIE, "Scissors should tie itself.")
expect(automatically_determine_winner("scissors", "paper") == RecognizedResult.PLAYER_1_WIN, "Scissors should beat paper.")
expect(automatically_determine_winner("scissors", "rock") == RecognizedResult.PLAYER_1_LOSS, "Scissors should lose to rock.")
expect(automatically_determine_winner("scissors", "bomb") == RecognizedResult.PLAYER_1_WIN, "Scissors should beat bomb.")
expect(automatically_determine_winner("scissors", "candy") == RecognizedResult.UNKNOWN, "Scissors should have an unknown result against odd input.")

expect(automatically_determine_winner("bomb", "scissors") == RecognizedResult.PLAYER_1_LOSS, "Bomb should lose to scissors.")
expect(automatically_determine_winner("bomb", "paper") == RecognizedResult.PLAYER_1_WIN, "Bomb should beat paper.")
expect(automatically_determine_winner("bomb", "rock") == RecognizedResult.PLAYER_1_WIN, "Bomb should beat rock.")
expect(automatically_determine_winner("bomb", "bomb") == RecognizedResult.TIE, "Bomb should tie bomb.")
expect(automatically_determine_winner("bomb", "candy") == RecognizedResult.UNKNOWN, "Rock should have an unknown result against odd input.")

expect(automatically_determine_winner("rock", "r") == RecognizedResult.TIE, "Aliasing should work for rock.")
expect(automatically_determine_winner("paper", "p") == RecognizedResult.TIE, "Aliasing should work for paper.")
expect(automatically_determine_winner("scissors", "s") == RecognizedResult.TIE, "Aliasing should work for scissors.")
expect(automatically_determine_winner("bomb", "b") == RecognizedResult.TIE, "Aliasing should work for bomb.")

expect(automatically_determine_winner("candy", "candy") == RecognizedResult.UNKNOWN, "Two unknown inputs are unknown, even if they're the same.")

async def run_tests():
await test_input_equality()
await test_input_list_equality()
await test_rps_logic()
Loading

0 comments on commit e9af538

Please sign in to comment.