Skip to content

Commit

Permalink
Release Version 0.2.2 (#8)
Browse files Browse the repository at this point in the history
Release Version 0.2.2
- Performed a large code structure refactor for massively improved unit testing coverage.
- Unit testing coverage increased from ~40% to 92%.
- Fixed a few minor edge cases that unit testing has found while performing the refactor.

Unit Testing details
- Mocks are now used where appropriate to "mimic" outputs from the Reddit API (via PRAW).
- Fixtures are now used for some repetitive setup/teardown operations (particularly with the database)
- Fixtures can now be imported from conftest.py
- Database state is now properly preserved between test runs

Other minor details for changes
- VSCode settings.json file has been updated for the Python Testing Framework VSCode plugin
- All constants were moved from main.py to config/config.py
- main.py now has considerably less logic in it (it only contains instantiation logic)
- core.py now processes most of the game rule logic, broken down into different functions for testability purposes.
  • Loading branch information
William Lam authored May 11, 2021
1 parent 5654fae commit 077e30e
Show file tree
Hide file tree
Showing 23 changed files with 1,146 additions and 572 deletions.
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"editor.formatOnSave": true,
"python.formatting.provider": "black"
"python.formatting.provider": "black",
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true
}
2 changes: 1 addition & 1 deletion hon_patch_notes_game_bot/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.1"
__version__ = "0.2.2"
158 changes: 128 additions & 30 deletions hon_patch_notes_game_bot/communications.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,85 @@
#!/usr/bin/python
"""
This module contains functions related to communications across the Reddit platform
"""
import time
import re
from typing import List
from typing import List, Tuple

from praw.exceptions import RedditAPIException
from praw import Reddit
from hon_patch_notes_game_bot.config.config import STAFF_MEMBER_THAT_HANDS_OUT_REWARDS
from praw.exceptions import RedditAPIException
from praw.models import Subreddit, Submission
from hon_patch_notes_game_bot.database import Database
from hon_patch_notes_game_bot.patch_notes_file_handler import PatchNotesFile
from hon_patch_notes_game_bot.utils import (
processed_submission_content,
processed_community_notes_thread_submission_content,
)
from hon_patch_notes_game_bot.config.config import (
COMMUNITY_SUBMISSION_TITLE,
STAFF_MEMBER_THAT_HANDS_OUT_REWARDS,
SUBMISSION_TITLE,
)


def init_submissions(
reddit: Reddit,
subreddit: Subreddit,
database: Database,
patch_notes_file: PatchNotesFile,
submission_content_path: str,
community_submission_content_path: str,
) -> Tuple[Submission, Submission]:
"""
Initializes the primary and community submission (i.e. "Reddit threads") objects.
If they do not exist in the database, then this function creates them.
Otherwise, it retrieves the submissions via their URL from the database.
Returns:
- A tuple containing the primary submission and community submission objects
"""
# Main submission
submission_content = processed_submission_content(
submission_content_path, patch_notes_file
)
submission: Submission = None
submission_url = database.get_submission_url(tag="main")

# Get main submission if it does not exist
if submission_url is None:
submission = subreddit.submit(
title=SUBMISSION_TITLE, selftext=submission_content
)
database.insert_submission_url("main", submission.url)
submission_url = submission.url
else:
# Obtain submission via URL
submission = reddit.submission(url=submission_url)

# Community submission
community_submission_content = processed_community_notes_thread_submission_content(
community_submission_content_path, patch_notes_file, submission_url
)
community_submission: Submission = None
community_submission_url = database.get_submission_url(tag="community")

# Get community submission if it does not exist
if community_submission_url is None:
community_submission = subreddit.submit(
title=COMMUNITY_SUBMISSION_TITLE, selftext=community_submission_content,
)
database.insert_submission_url("community", community_submission.url)

# Update main Reddit Thread's in-line URL to connect to the community submission URL
updated_text = submission.selftext.replace(
"#community-patch-notes-thread-url", community_submission.url
)
submission.edit(body=updated_text)
else:
# Obtain submission via URL
community_submission = reddit.submission(url=community_submission_url)

return submission, community_submission


def send_message_to_staff(
Expand Down Expand Up @@ -44,13 +116,15 @@ def send_message_to_staff(
subject=subject_line, message=winners_list_text
)
except RedditAPIException as redditError:
print(f"RedditAPIException encountered: {redditError}")
print(
f"{redditError}\n{recipient} was not sent a message, continuing to next recipient"
f"{recipient} was not sent a message, continuing to next recipient"
)
continue
except Exception as error:
print(f"General Exception encountered: {error}")
print(
f"{error}\n{recipient} was not sent a message, continuing to next recipient"
f"{recipient} was not sent a message, continuing to next recipient"
)
continue

Expand All @@ -63,6 +137,16 @@ def send_message_to_winners(
This function uses recursion to send messages to failed recipients.
This function also frequently encounters Reddit API Exceptions due to rate limits.
To sleep for the appropriate duration without wasting time, the rate limit error is parsed:
Test strings for regex capture:
RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes before trying again." on field 'ratelimit'
RATELIMIT: "Looks like you've been doing that a lot. Take a break for 47 seconds before trying again." on field 'ratelimit'
RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes 47 seconds before trying again."
on field 'ratelimit'
RATELIMIT: "Looks like you've been doing that a lot. Take a break for 1 minute before trying again." on field 'ratelimit'
Attributes:
reddit: the PRAW Reddit instance
winners_list: a list of winning recipients for the PM
Expand All @@ -80,6 +164,7 @@ def send_message_to_winners(
f"You have been chosen by the bot as a winner for the {version_string} Patch Notes Guessing Game!\n\n"
f"Please send /u/{STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} a Private Message (PM) to request a code"
f" for {str(gold_coin_reward)} Gold Coins.\n\n"
"Be sure to check your Reddit Chat inbox as well (not just your Reddit mail inbox)!\n\n"
"Thank you for participating in the game! =)"
)
try:
Expand All @@ -88,30 +173,40 @@ def send_message_to_winners(

# Reddit API Exception
except RedditAPIException as redditException:
failed_recipients_list.append(recipient)
print(
f"{redditException}\n{recipient} was not sent a message (added to retry list), continuing to next recipient"
)

# Rate limit error handling
if redditException.error_type == "RATELIMIT":
# Error printouts for debugging
print(f"Full Reddit Exception: {redditException}\n\n")
print(f"Reddit Exception Item 0: {redditException.items[0]}\n\n")

# Sleep for the rate limit duration by parsing the minute count from exception message
regex_capture = re.search(
r"(\d+) minutes", redditException.items[0].message
)

if regex_capture is None:
print("Invalid regex detected. Sleeping for 60 seconds...")
time.sleep(60)
else:
minutesToSleep = regex_capture.group(1)
secondsToSleep = int(minutesToSleep) * 60
print("Sleeping for " + str(secondsToSleep) + " seconds")
time.sleep(secondsToSleep)
print(f"Full Reddit Exception: {redditException}\n\n")

for subException in redditException.items:
# Rate limit error handling
if subException.error_type == "RATELIMIT":
failed_recipients_list.append(recipient)
print(
f"{redditException}\n{recipient} was not sent a message (added to retry list), "
"continuing to next recipient"
)
print(f"Subexception: {subException}\n\n")

# Sleep for the rate limit duration by parsing the minute and seconds count from
# the message into named groups
regex_capture = re.search(
r"\s+((?P<minutes>\d+) minutes?)?\s?((?P<seconds>\d+) seconds)?\s+",
subException.message,
)
if regex_capture is None:
print("Invalid regex detected. Sleeping for 60 seconds...")
time.sleep(60)
break
else:
# Use named groups from regex capture and assign them to a dictionary
sleep_time_regex_groups = regex_capture.groupdict(default=0)
secondsToSleep = int(
sleep_time_regex_groups.get("minutes") # type: ignore
) + int(
sleep_time_regex_groups.get("seconds") # type: ignore
) # type: ignore

print(f"Sleeping for {str(secondsToSleep)} seconds")
time.sleep(secondsToSleep)
break

continue

Expand All @@ -123,7 +218,10 @@ def send_message_to_winners(

# At the end of the function, recurse this function to re-send messages to failed recipients
# Recurse only if failed_recipients_list has content in it
if len(failed_recipients_list) > 0:
# Prevents infinite loops by ensuring that the failed recipients count
# gradually progresses towards the end condition.
failed_recipients = len(failed_recipients_list)
if failed_recipients > 0 and failed_recipients < len(winners_list):
send_message_to_winners(
reddit, failed_recipients_list, version_string, gold_coin_reward
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
This thread contains the results of the community-compiled Patch Notes from the main Guessing Game thread.

- Note that the patch notes published here are subject to change at any time after this game has been set live. However, they are unlikely to change much (if at all).

**Guesses in this thread will not be responded to by the bot. [Visit the main thread instead!](#main-reddit-thread)**

Feel free to discuss patch changes here liberally (based on the currently revealed notes)! :)
22 changes: 15 additions & 7 deletions hon_patch_notes_game_bot/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
# ==========

# [GAME] 4.9.X - Patch Notes Guessing Game
submission_title: str = "[GAME] 4.9.3 - Patch Notes Guessing Game"
community_submission_title: str = "4.9.3 - Community-revealed Patch Notes"
game_end_time: str = "May 10, 2021, 4:00 am UTC"
SUBMISSION_TITLE: str = "[GAME] 4.9.3 - Patch Notes Guessing Game"
COMMUNITY_SUBMISSION_TITLE: str = "4.9.3 - Community-revealed Patch Notes"
GAME_END_TIME: str = "May 10, 2021, 4:00 am UTC"

# ================
# Reward settings
# ================

gold_coin_reward: int = 300
GOLD_COIN_REWARD: int = 300
NUM_WINNERS: int = 25

# ================
Expand All @@ -31,12 +31,20 @@
SLEEP_INTERVAL_SECONDS: int = 10
STAFF_MEMBER_THAT_HANDS_OUT_REWARDS: str = "FB-Saphirez"

BOT_USERNAME: str = "hon-bot"
USER_AGENT: str = "HoN Patch Notes Game Bot by /u/hon-bot"
PATCH_NOTES_PATH: str = "config/patch_notes.txt"
SUBMISSION_CONTENT_PATH: str = "config/submission_content.md"
COMMUNITY_SUBMISSION_CONTENT_PATH: str = "config/community_patch_notes_compilation.md"
WINNERS_LIST_FILE_PATH: str = "cache/winners_list.txt"
BLANK_LINE_REPLACEMENT: str = "..."

# ================
# Data structures
# ================

# Disallowed users: use a set for the O(1) access time
disallowed_users_set: Set[str] = {
DISALLOWED_USERS_SET: Set[str] = {
"ElementUser",
"FB-Saphirez",
"the_timezone_bot",
Expand All @@ -50,7 +58,7 @@
}

# Recipients user list (for Private Messages) for the winners list
staff_recipients: List[str] = ["ElementUser", "FB-Saphirez"]
STAFF_RECIPIENTS_LIST: List[str] = ["ElementUser", "FB-Saphirez"]

# Invalid line strings (for guess validity in the game)
invalid_line_strings: List[str] = ["_______", "-------"]
INVALID_LINE_STRINGS: List[str] = ["_______", "-------"]
10 changes: 5 additions & 5 deletions hon_patch_notes_game_bot/config/submission_content.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
It's time for the HoN Patch Notes guessing game again!

HoN Patch Version: `patch_version`
HoN Patch Version: `PATCH_VERSION`

This game will automatically stop running at **`game_end_time`**, or when **`MAX_PERCENT_OF_LINES_REVEALED`% of the total number of lines in the patch notes are revealed**. The bot will stop monitoring and responding to comments in this thread after either of these conditions are met.
This game will automatically stop running at **`GAME_END_TIME`**, or when **`MAX_PERCENT_OF_LINES_REVEALED`% of the total number of lines in the patch notes are revealed**. The bot will stop monitoring and responding to comments in this thread after either of these conditions are met.

## Rules of the Game

- Pick a number between 1 and `max_line_count`, and post that number to the main thread or the comment that /u/hon-bot responds to you with.
- Pick a number between 1 and `MAX_LINE_COUNT`, and post that number to the main thread or the comment that /u/hon-bot responds to you with.
- Guesses for lines that actually have content in the patch notes are considered "valid guesses", and the user will be entered into the pool of potential winners for a prize if they get a valid guess! See the Rewards section for more information.
- Each user gets `max_num_guesses` guesses until they run out of guesses.
- Each user gets `MAX_NUM_GUESSES` guesses until they run out of guesses.
- If your guess has a number in it in your first line of your comment, it WILL be parsed by the bot and will count as a guess (whether you want it to or not). For simplicity's sake, please only include a number in your guess.
- Guesses for line numbers that don't exist in the patch notes count as an invalid guess. You have been warned!
- There are invalid lines in the patch notes. These are blank lines, and lines with separator elements like `_______` and `-------`.
Expand All @@ -25,7 +25,7 @@ This game will automatically stop running at **`game_end_time`**, or when **`MAX

## Rewards

- `num_winners` Lucky Winners from the pool of potential winners will receive `gold_coin_reward` Gold Coins each (NAEU/International Client only - SEA players will not get a working code if they win, unfortunately).
- `NUM_WINNERS` Lucky Winners from the pool of potential winners will receive `GOLD_COIN_REWARD` Gold Coins each (NAEU/International Client only - SEA players will not get a working code if they win, unfortunately).
- Winners will be announced in this thread when the game ends.
- You will receive a message from /u/hon-bot if you have been chosen!

Expand Down
Loading

0 comments on commit 077e30e

Please sign in to comment.