Gone Fishin'!
Gone Fishin’! is a small little project I worked on during December 2022.
It takes use of the Discord interaction system to simulate the fishing algorithm from Toontown Online. Player data is stored on a database for persistent sessions.
Previews
Technical Information
This was my first project where I had to use CRUD with a database. It uses SQLite as a database, SQLAlchemy for the backend, and discord.py for the frontend.
The frontend was originally going to use the Rich library, but I decided later on to switch to Discord.py for accessibility and to play with others.
DB Structure
I used a blob data type to store player data. The blob was a FishContext object that held values. I am NOT a fan of what I did here, since the blob updating meant that one hiccup can completely corrupt an entry. Nonetheless though, I was still learning about database creation at the time.
Here’s a snippet of the DatabaseManager module, used to call and update database entries.
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key = True) # user number
disid = Column(Integer) # discord id
username = Column(String) # friendly username
context = Column(db.PickleType(), nullable = True) # FishContext object, blob
def __init__(self, disid: int, username: str):
self.disid = disid
self.username = username
def __repr__(self):
return "<User(disid='%s', context='%s', id='%s')>" % (
self.disid,
self.context,
self.id,
)
def registerFishContext(self, cls):
self.context = cls
return cls
def updateContext(disid, context):
session.execute(
update(User).where(User.disid == disid).values(context = context)
)
session.commit()

The FishContext Object
The following is a snippet of the FishContext class, which would be stored in a blob:
class FishContext(object):
_db = None # this _db variable is required. do not remove!
disid = 0 # discord id
# session-based values
_CAUGHT_FISH_SESSION: int = 0 # should always be 0 in beginning of the session
_MENU_MODE: MainMenuChoice = MainMenuChoice.NONE # show stats or play game?
_GAME_MODE: GameMode = GameMode.NONE # -1 if not game
_SESSION_MENU: SessionMenu.NONE
_NEW_SPECIES: bool = False # resets every cast
_NEW_RECORD: bool = False # resets every cast
_IN_TUTORIAL: int = False
_TUTORIAL_DIALOGUE_ID: int = 0
_NEW_PLAYER: int = True
_LEVELS_UNLOCKED: int # [0] * len(LocationData), access is LocationData Key - 1
_ROD_ID: FishingRod.TWIG_ROD
_LOCATION_ID: int = Location.NONE # currently selected location wrt LocationData
_BUCKET_CONTENTS: list = [] # [['Sad Clown Fish', 40, 13]] -> [caughtFish, weight, fishValue]
_BUCKET_SIZE: int = 0 # current amt of fish in your bucket
_BUCKET_SIZE_MAX: int = 20 # how many fish can be held in your bucket at once, default is 20
_BUCKET_FULL: bool = False
_JELLYBEANS_TOTAL: int = 0 # "in bank"
_JELLYBEANS_CURRENT: int = 0 # accumulated from bucket
# Cheats / Debug entries
_USE_FISHING_BUCKET: bool = True
# WARNING: keys may not be sorted in numerical order
_FISH_DATA: dict
"""
# includes caught species
# intial dict is empty
_FISH_DATA: dict = {
FISH_GENUS_1 {
FISH_SPECIES_1: [
[SMALLEST_WEIGHT: int, LARGEST_WEIGHT: int], [caughtrod1, caughtrod2, caughtrod3],
[other personal data entries go here]
],
}
}
"""
Discord “Frontend”
The FishingSimDiscordBot module manages the Discord API connection alongside the user interface.
When a user activates the bot via /gofish
, we check to see if they are in our database & act correspondingly:
class MasterView(discord.ui.View):
# ...
def __init__(self, user: discord.User):
"""
:param user: user reserved for this interaction
"""
super().__init__()
self.user = user.id
# our initial call to init and communicate with the database
db = DatabaseManager
# Check to see if user is registered in database
user_db = (db.session.query(db.User).filter_by(disid=self.user).first())
if not user_db:
# register the new user
user = db.User(self.user, str(user))
self.context = user.registerFishContext(FishContext()) # returns FishContext
db.session.add(user)
db.session.commit()
# todo, move these to a check to see if user is new to campaign
# since they are a new user, let's apply some default values:
self.context.JELLYBEANS_TOTAL = 20 # to start them off, give them 20 jellybeans.
# or maybe we can just give them a grace cast ^
self.context.BUCKET_SIZE_MAX = 20 # default bucket size
self.context.ROD_ID = FishingRod.TWIG_ROD # beginner rod
else:
# nope, he's already registered with us, register the pre-existing context blob.
self.context = user_db.context
# set our db marker if haven't already, this is done upon the first command executed in the bot session
FishInternal.db = db
# for convenience of db updates, we give the context object a pointer to the userid.
self.context.disid = self.user
# let's ensure that session-based values are their default values:
self.context.CAUGHT_FISH_SESSION = 0
# also, ensure some default values exist for us.
self.adjust_context_entries()
# since we've modified context, might as well register the disid value into the context blob
db.updateContext(self.user, self.context)
# now that everything's ready to go, we can show the user their next options.
self.main_menu()