Gone Fishin'!

Gone Fishin! Logo

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()

Database schema.

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()
Avatar
Loonatic
Technical Artist, Texture Artist

I’m me.

Next
Previous