1
\$\begingroup\$

As a final project for a software engineering bootcamp, I decided to make a text-based multiplayer RPG game engine. Using Python, Flask, & Flask-SocketIO for the backend and React & TypeScript for the frontend I've gotten my project to a point where it functions (mostly) the way I imagined initially. However, I want to continue developing the game on the side as a hobby, and I feel like there are numerous things that should be optimized before I start adding more functionality to it.

Link to my GitHub repo

Link to the deployed website

A few things I feel like could be improved:

  1. Combat is currently a caricature of combat. I'm still trying to figure out the best way to handle engaging another entity in combat and the process behind retaliating against an entity attacking you
  2. The way that objects are instantiated into the world currently doesn't lend itself to world persistence. Is pickling/dilling/shelving every object the best way to handle saving the world, or should I be using a db?
  3. The classes for all entities. I'm unsure if I'm duplicating information with the way I reference certain objects, and if all the attributes are necessary.
  4. Text input parsing. Currently my parsing is just splitting the input from a user into the first word and the rest of the data, and filtering it through many "if" statements. Is there a more efficient way of handling this?

Most relevant code:

Backend:

events.py: run when users connect or disconnect

from flask import session, request, current_app
from ... import socketio
from flask_socketio import join_room, leave_room, emit
from app.models import PlayerAccount, User
import app.blueprints.main.objects as objects
import dill


#Socket IO events, should only be three. On connection, on disconnect, and whenever data is sent
client_list = [] #List of clients currently connected
world = objects.World() #Instatiating world class to hold all rooms, players, and characters

def world_timer(app):
     print('world timer triggered')
     socketio.sleep(10)
     with app.app_context():
        while True:
                if client_list:
                    socketio.sleep(10)
                    for character in world.npcs.values():
                        character.ambiance()
                    for room in world.rooms.values():
                        room.ambiance()
                    for player in world.players.values():
                        player_account = PlayerAccount.query.get(player.id)
                        player_account.player_info = dill.dumps(player)
                        player_account.save()

                            
                else:
                    world.timer_active = False
                    print('world timer deactivated')
                    break


#This is an event that occurs whenever a new connection is detected by the socketio server. Connection needs to properly connect the user with their Player object, update the Player object's session_id so private server emits can be transmitted to that player only
@socketio.on('connect')
def connect(auth):
    session['user_id'] = auth
    print(session['user_id'])
    current_user = User.query.filter(User.id == auth).first()
    active_player = current_user.accounts.filter(PlayerAccount.is_active == True).first() #Pulls the active player information
    if not active_player.player_info: #Checks to see if the active player is a new player
        player = objects.Player(id=active_player.id, name=active_player.player_name, description="A newborn player, fresh to the world.", account=active_player.user_id)
        #Creates a new player object
        active_player.player_info = dill.dumps(player) #Pickles and writes new player object to active player info
        active_player.save() #Saves pickled data to player database
    else:
        player = dill.loads(active_player.player_info, ignore=False) #Loads pickled data in to the player
    username = player.name
    location = player.location
    player.session_id = request.sid
    world.players.update({player.id: player})
    if not world.timer_active:
        world.timer_active = True
        socketio.start_background_task(world_timer, current_app._get_current_object())
    client_list.append(player.session_id)
    print(f'client list is {client_list}')
    print(f'players connected is {world.players}')
    session['player_id'] = player.id
    session['player'] = player
    join_room(location)
    player.location_map()
    player.inventory_update()
    socketio.emit('event', {'message': f'{username} has connected to the server'})
    socketio.emit('event', {'message': f'{username} appears with a puff of smoke.'}, to=location, skip_sid=player.session_id)
    player.connection()

#Event that handles disconnection. Unsure if I should be saving player information on disconnect or periodically. Likely both. Need to remove the player and client from the list of active connections. If all players are disconnected, world state should be saved and server activity spun down.
@socketio.on('disconnect')
def disconnect():
    player_id = int(session['player_id'])
    player_account = PlayerAccount.query.get(player_id)
    player = world.players[player_id]
    room = player.location
    client_list.pop(client_list.index(player.session_id))
    player_account.player_info = dill.dumps(player)
    player_account.save()
    del world.players[player_id]
    leave_room(room)
    player.disconnection()
    socketio.emit('event', {'message': f'{player.name} has left the server'})
    socketio.emit('event', {'message': f'{player.name} disappears, leaving slight wisps of smoke behind.'}, to=room)

objects.py: All entities in the game

from ... import socketio
from flask import request
import app.blueprints.main.events as events
import dill
import itertools
from flask_socketio import join_room, leave_room
import app.blueprints.main.rooms as room_file
import app.blueprints.main.NPCs as npc_file
import app.blueprints.main.items as item_file
from random import randint

#World class that holds all entities
class World():
    def __init__(self) -> None:
        self.timer_active = False
        rooms = {}
        for room in room_file.room_dict.values():
            new_room = Room(room['name'], room['description'], room['position'], room['exits'], room['icon'], room['ambiance_list'])
            rooms.update({new_room.position: new_room})
        self.rooms = rooms
        self.players = {}

        npcs = {}
        for npc in npc_file.npc_dict.values():
            new_npc = NPC(npc['name'], npc['aliases'], npc['description'], npc['deceased'], npc['health'], npc['level'], npc['location'], npc['home'], npc['ambiance_list'], npc['can_wander'], npc['attackable'])
            self.rooms[new_npc.location].contents['NPCs'].update({new_npc.id: new_npc})
            npcs.update({new_npc.id: new_npc})
        self.npcs = npcs

        for item in item_file.item_dict.values():
            new_item = Item(item['name'], item['description'], item['aliases'], item['group'], item['weight'], item['location'])
            for room_id, room in self.rooms.items():
                if new_item.location == room_id:
                    room.contents['Items'].update({new_item.id: new_item})
                    print(f'added {new_item.name} to {room.name}. Room inventory is {room.contents}')

    def world_test(self):
        for room in self.rooms.values():
            print(id(room), room.name, room.contents)

    def world_who(self, player):
        player_list = []
        for player_who in self.players.values():
            player_list.append(player_who.name)
        output = '<br/> Currently Online Players: <br/>'
        for player_who in player_list:
            output += f'{player_who}<br/>'
        socketio.emit('event', {'message': output}, to=player.session_id)

    #Might not use this, and instead use individual pickles for each entity type for modularization
    # def world_save(self):
    #     with open('app/data/world_db.pkl', 'wb') as dill_file:
    #         dill.dump(self, dill_file)
    #     socketio.emit('event', {'message': 'world saved'})

    # def room_save(self):
    #     with open('app/data/room_db.pkl', 'wb') as dill_file:
    #         dill.dump(self.rooms, dill_file)
    #     socketio.emit('event', {'message': 'rooms saved'})


#Overall class for any interactable object in the world
class Entity():
    def __init__(self, name, description) -> None:
        self.name = name #Shorthand name for an entity
        self.description = description #Every entity needs to be able to be looked at

    #Test function currently, but every entity needs to be able to describe itself when looked at
    def describe(self):
        pass

#Class for rooms. Rooms should contain all other objects (NPCs, Items, Players, anything else that gets added)
class Room(Entity):
    id = itertools.count()
    def __init__(self, name, description, position, exits, icon, ambiance_list) -> None:
        super().__init__(name, description)
        self.id = next(Room.id)
        self.position = position #Coordinates in the grid system for a room, will be used when a character moves rooms
        self.exits = exits #List of rooms that are connected to this room. Should be N,S,E,W but may expand so a player can "move/go shop or someting along those lines"
        self.icon = icon #Icon for the world map, should consist of two ASCII characters (ie: "/\" for a mountain)
        self.contents = {'NPCs': {}, 'Players': {}, 'Items': {}} #Dictionary containing all NPCs, Players, and Items currently in the room. Values will be modified depending on character movement, NPC generation, and item movement
        self.ambiance_list = ambiance_list

    def describe_exits(self):
        output = '<strong>[</strong> Exits: '
        for each in self.exits:
            output += f'⟪<strong>{each}</strong>⟫ '
        output += '<strong>]</strong>'
        return output

    def describe_contents(self, caller):
        output = ''
        item_output = ''
        player_list = dict(self.contents['Players'])
        del player_list[caller.id]
        player_key = list(enumerate(player_list))
        item_list = dict(self.contents['Items'])
        item_key = list(enumerate(item_list))
        npc_list = dict(self.contents['NPCs'])
        corpse_list = {}
        for id, npc in npc_list.copy().items():
            if npc.deceased:
                corpse_list[id] = npc_list.pop(id)
        corpse_key = list(enumerate(corpse_list))
        npc_key = list(enumerate(npc_list))
        total_list = []
        total_corpses = []
        total_items = []
        for i in range(len(player_list)):
            total_list.append(f'<em><strong>{player_list[player_key[i][1]].name}</strong></em>')
        for i in range(len(npc_list)):
            total_list.append(npc_list[npc_key[i][1]].name)
        for i in range(len(corpse_list)):
            total_corpses.append(corpse_list[corpse_key[i][1]].name)
        for i in range(len(item_list)):
            total_items.append(item_list[item_key[i][1]].name)
        if len(total_list) == 0:
            pass
        elif len(total_list) == 1:
            output += f'{total_list[0]} stands here.'
        elif len(total_list) == 2:
            output += f'{total_list[0]} and {total_list[1]} stand here.'
        else:
            for i in range(len(total_list)):
                if i == len(total_list)-1:
                    output += f'and {total_list[i]} stand here.'
                else: output += f'{total_list[i]}, '

        if len(total_corpses) == 0:
            pass
        elif len(total_corpses) == 1:
            output += f' The body of {total_corpses[0]} is on the ground here.'
        elif len(total_corpses) == 2:
            output += f' The bodies of {total_corpses[0]} and {total_corpses[1]} lay here.'
        else:
            for i in range(len(total_corpses)):
                if i == 0:
                    item_output += f'The bodies of {total_corpses[i]}, '
                if i == len(total_corpses)-1:
                    item_output += f'and {total_corpses[i]} lay here.'
                else: item_output += f'{total_corpses[i]}, '


        if len(total_items) == 0:
            pass
        elif len(total_items) == 1:
            item_output += f' There is a {total_items[0]} here.'
        elif len(total_items) == 2:
            item_output += f' There is a {total_items[0]} and a {total_items[1]} here.'
        else:
            for i in range(len(total_items)):
                if i == 0:
                    item_output += f'A {total_items[i]}, '
                if i == len(total_items)-1:
                    item_output += f'and {total_items[i]} lay here.'
                else: item_output += f'a {total_items[i]}, '

        
        
        socketio.emit('event', {'message': output}, to=caller.session_id)
        socketio.emit('event', {'message': item_output}, to=caller.session_id)
        socketio.emit('event', {'message': self.describe_exits()}, to=caller.session_id)

            

    def ambiance(self):
        if randint(1,5) == 5:
            socketio.emit('event', {'message': f'{self.ambiance_list[randint(0,len(self.ambiance_list)-1)]}'}, to=self.position)


#Broad class for any entity capable of independent and autonomous action that affects the world in some way
default_stats = {
'strength': 10,
'endurance': 10,
'intelligence': 10,
'wisdom': 10,
'charisma': 10,
'agility': 10
}
class Character(Entity):
    def __init__(self, name, description, health, level, location, stats, deceased) -> None:
        super().__init__(name, description)
        self.health = health #All characters should have a health value
        self.level = level #All characters should have a level value
        self.location = location #All characters should have a location, reflecting their current room and referenced when moving
        self.stats = stats #All characters should have a stat block.
        self.deceased = deceased #Indicator of if a character is alive or not. If True, inventory can be looted
        self.inventory = [] #List of items in character's inventory. May swap to a dictionary of lists so items can be placed in categories


#Class that users control to interact with the world. Unsure if I need to have this mixed in with the models side or if it would be easier to pickle the entire class and pass that to the database?
class Player(Character):
    def __init__(self, id, account, name, description, health=100, level=1, location='0,0', stats=dict(default_stats), deceased=False) -> None:
        super().__init__(name, description, health, level, location, stats, deceased)
        self.id = id
        self.account = account #User account associated with the player character
        self.session_id = '' #Session ID so messages can be broadcast to players without other members of a room or server seeing the message. Session ID is unique to every connection, so part of the connection process must be to assign the new value to the player's session_id
        self.inventory = list([])
        self.player_map = ''
        self.in_combat = False

    def connection(self):
        events.world.rooms[self.location].contents['Players'].update({self.id: self})
        socketio.emit('event', {'message': f'<br/><strong>{events.world.rooms[self.location].name}</strong>'}, to=self.session_id)
        socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
        events.world.rooms[self.location].describe_contents(self)

    def disconnection(self):
        del events.world.rooms[self.location].contents['Players'][self.id]

    # def player_save(self):
    #         player_account = PlayerAccount.query.get(self.id)
    #         player_account.player_info = dill.dumps(self)
    #         player_account.save()
    #     print(f'{self.name} saved')
    #     return

    def set_description(self, data):
        self.description = data
        socketio.emit('event', {'message': f'Your description has been updated to "{self.description}"'}, to=self.session_id)

    def look(self, data, room):
        if data == '':
            # socketio.emit('event', {'message': self.location}, to=self.session_id)
            socketio.emit('event', {'message': f'<br/><strong>{room.name}</strong>'}, to=self.session_id)
            socketio.emit('event', {'message': room.description}, to=self.session_id)
            room.describe_contents(self)
        elif data in ['me', 'myself']:
            socketio.emit('event', {'message': 'You really want me to describe you, to <em>you</em>? You really are something. Get your kicks somewhere else, bucko.'}, to=self.session_id)
        else:
            for player in room.contents['Players'].values():
                if player == self:
                    continue
                if player.name.lower().startswith(data.lower()):
                    socketio.emit('event', {'message': player.description}, to=self.session_id)
                    return
            for npc in room.contents['NPCs'].values():
                for alias in npc.aliases:
                    if alias.lower().startswith(data.lower()):
                        if npc.deceased:
                            socketio.emit('event', {'message': npc.description_deceased}, to=self.session_id)
                            return
                        socketio.emit('event', {'message': npc.description}, to=self.session_id)
                        return
            for item in room.contents['Items'].values():
                for alias in item.aliases:
                    if alias.lower().startswith(data.lower()):
                        socketio.emit('event', {'message': item.description}, to=self.session_id)
                        return
            for direction, room in room.exits.items():
                if direction.lower().startswith(data.lower()):
                    if events.world.rooms[room].name.startswith('The'):
                        socketio.emit('event', {'message': f'{events.world.rooms[room].name} lies that way.'}, to=self.session_id)
                        return
                    socketio.emit('event', {'message': f'The {events.world.rooms[room].name} lies that way.'}, to=self.session_id)
                    return
                
    def location_map(self):
        self_map = []
        if '0,' not in self.location and '1,' not in self.location and '2,' not in self.location and '3,' not in self.location and '4,' not in self.location and '5,' not in self.location and '6,' not in self.location and '7,' not in self.location and '8,' not in self.location and '9,' not in self.location:
            for i in range(1,10):
                if i != 5:
                    if i == 3:
                        self_map.append('<span style="background-color: tan">&nbsp;&nbsp;</span><br>')
                    elif i == 6:
                        self_map.append('<span style="background-color: tan">&nbsp;&nbsp;</span><br>')
                    else:
                        self_map.append('<span style="background-color: tan">&nbsp;&nbsp;</span>')
                else: self_map.append('<span style="background-color:LightBlue; color: DodgerBlue">()</span>')
        else:
            for i in range(1,10):
                lon, lat = self.location.split(',', 1)
                lon = int(lon)
                lat = int(lat)
                if i == 1:
                    lat += 1
                    lon -= 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
                if i == 2:
                    lat += 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
                if i == 3:
                    lat += 1
                    lon += 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}<br>')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span><br>')
                if i == 4:
                    lon -= 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
                if i == 5:
                    self_map.append('<span class="map-square" style="background-color:LightBlue; color: DodgerBlue">()</span>')
                if i == 6:
                    lon += 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}<br>')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span><br>')
                if i == 7:
                    lat -= 1
                    lon -= 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
                if i == 8:
                    lat -= 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
                if i == 9:
                    lat -= 1
                    lon += 1
                    try:
                        self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
                    except KeyError:
                        self_map.append('<span class="map-square" style="background-color: tan">&nbsp;&nbsp;</span>')
        output = '<tt class="map-square" style="margin-bottom: 0">'
        for icon in self_map:
            output += icon
        output += '</tt>'
        self.player_map = output
        socketio.emit('map', {'map': self.player_map}, to=self.session_id)
        
    
    def speak(self, data):
        socketio.emit('event', {'message': f'{self.name} says "{data}"'}, room=self.location, include_self=False)
        socketio.emit('event', {'message': f'You say "{data}"'}, to=self.session_id)

    def whisper(self, whisper_player, data):
        socketio.emit('event', {'message': f'The wind breathes against your ear, and you can faintly hear "{data}". You get a feeling it\'s from {self.name}.'}, to=whisper_player.session_id)
        socketio.emit('event', {'message': 'You whisper your message to the wind.'}, to=self.session_id)

    def combat(self, victim):
        if not victim.attackable:
            print()
            socketio.emit('event', {'message': 'It\'s ok. I get it. Sometimes someone says something and it just makes you so angry. Unfortunately, whoever you\'re currently targeted with your untethered rage is just so important we can\'t let you hurt them. Sorry bud, go punch a tree.'}, to=self.session_id)
            return
        if isinstance(victim, NPC):
            if victim.deceased:
                socketio.emit('event', {'message': 'Jesus man they\'re already dead calm down. Go talk to a psychiatrist or something.'}, to=self.session_id)
            else:
                socketio.emit('event', {'message': f'{self.name} slams their fist into {victim.name}, killing them instantly.'}, to=self.location, skip_sid=self.session_id)
                socketio.emit('event', {'message': f'You slam your fist into {victim.name}, killing them instantly.'}, to=self.session_id)
                victim.deceased = True
                return

    def move(self, direction, room):
        if direction not in room.exits:
            socketio.emit('event', {'message': 'You can\'t go that way.'}, to=self.session_id)
            return
        leave_room(self.location)
        if direction in ['out', 'in']:
            socketio.emit('event', {'message': f'{self.name} moves towards the {room.exits[direction]}'}, room=self.location)
        else:
            socketio.emit('event', {'message': f'{self.name} moves towards the {direction}'}, room=self.location)
        if self.id in room.contents['Players']:
            del room.contents['Players'][self.id]
        if direction in ['out', 'in']:
            socketio.emit('event', {'message': f'You move towards the {events.world.rooms[room.exits[direction]].name}'}, to=self.session_id)
        else:
            socketio.emit('event', {'message': f'You move towards the {direction}'}, to=self.session_id)

        new_location = room.exits[direction]
        if direction in ['out', 'in']:
            print(events.world.rooms[new_location].exits)
            came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location]
        else:
            came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location]
        socketio.emit('event', {'message': f'{self.name} arrives from the {came_from[0]}'}, room=new_location)
        socketio.sleep(.5)
        self.location = new_location
        join_room(self.location)
        self.location_map()
        events.world.rooms[self.location].contents['Players'].update({self.id: self})
        socketio.emit('event', {'message': f'<br/><strong>{events.world.rooms[self.location].name}</strong>'}, to=self.session_id)
        socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
        events.world.rooms[self.location].describe_contents(self)
    
    def inventory_update(self):
        inventory_send = []
        for item_inv in self.inventory:
                item_info = {
                    'id': item_inv.id,
                    'name': item_inv.name,
                    'group': item_inv.group
                }
                inventory_send.append(item_info)
        socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)

    def inventory_display(self):
        inventory_send = []
        if not self.inventory:
            socketio.emit('event', {'message': 'Sorry bud, you don\'t have anything in your inventory right now.'}, to=self.session_id)
        else:
            output = '<br/>Inventory:<br/>'
            for item_inv in self.inventory:
                item_info = {
                    'id': item_inv.id,
                    'name': item_inv.name,
                    'group': item_inv.group
                }
                output += f'{item_inv.name}<br/>'
                inventory_send.append(item_info)
            print(inventory_send)
            socketio.emit('event', {'message': output}, to=self.session_id)
            socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)

    def item_get(self, item, room):
        inventory_send = []
        for item_id, room_item in room.contents['Items'].copy().items():
            if item in room_item.aliases:
                self.inventory.append(room.contents['Items'].pop(item_id))
                socketio.emit('event', {'message': f'You get the {room_item.name}.'}, to=self.session_id)
                socketio.emit('event', {'message': f'You see {self.name} grab a {room_item.name}.'}, to=self.location, skip_sid=self.session_id)
                for item_inv in self.inventory:
                    item_info = {
                        'id': item_inv.id,
                        'name': item_inv.name,
                        'group': item_inv.group
                    }
                    inventory_send.append(item_info)
                socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
                return
        socketio.emit('event', {'message': f'I have absolutely no clue what you\'re trying to get here. I don\'t see a "{item}" here.'}, to=self.session_id)

    def item_drop(self, item, room):
        inventory_send = []
        for i in range(len(self.inventory.copy())):
            for alias in self.inventory[i].aliases:
                if item == alias:
                    drop_item = self.inventory.pop(i)
                    room.contents['Items'][drop_item.id] = drop_item
                    socketio.emit('event', {'message': f'You drop the {drop_item.name}.'}, to=self.session_id)
                    socketio.emit('event', {'message': f'You see {self.name} drop a {drop_item.name}.'}, to=self.location, skip_sid=self.session_id)
                    for item_inv in self.inventory:
                        item_info = {
                            'id': item_inv.id,
                            'name': item_inv.name,
                            'group': item_inv.group
                        }
                        inventory_send.append(item_info)
                    socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
                    return
        socketio.emit('event', {'message': f'Check your pockets, cause I don\'t see a "{item}" in your inventory.'}, to=self.session_id)

#Class that is controlled by the server. Capable of being interacted with.
class NPC(Character):
    id = itertools.count()
    def __init__(self, name, aliases, description, deceased, health, level, location, home, ambiance_list, can_wander, attackable, stats=dict(default_stats)) -> None:
        self.id = next(NPC.id)
        self.name = name
        self.aliases = aliases
        self.description = description
        self.description_deceased = f'The corpse of {self.name}, crumpled and torn.'
        self.deceased = deceased
        self.health = health
        self.level = level
        self.location = location
        self.home = home #Spawn location if NPC is killed. Can also double as a bound to prevent NPC from wandering too far from home during world timer movement
        self.ambiance_list = ambiance_list
        self.can_wander = can_wander
        self.inventory = list([])
        self.in_combat = False
        self.attackable = attackable

    def ambiance(self):
        if self.deceased:
            return
        numb = randint(1,5)
        if numb == 5:
            socketio.emit('event', {'message': f'{self.ambiance_list[randint(0,len(self.ambiance_list)-1)]}'}, to=self.location)
        elif numb == 4:
            if self.can_wander:
                lon, lat = self.location.split(',', 1)
                lon = int(lon)
                lat = int(lat)
                move_x = randint(-1,1)
                move_y = randint(-1,1)
                lon += move_x
                lat += move_y
                new_location = f'{lon},{lat}'
                for direction, coords in events.world.rooms[self.location].exits.items():
                    if new_location == coords:
                        socketio.emit('event', {'message': f'{self.name} wanders away towards the {direction}.'}, to=self.location)
                        del events.world.rooms[self.location].contents['NPCs'][self.id]
                        self.location=new_location
                        events.world.rooms[self.location].contents['NPCs'].update({self.id: self})
                        socketio.emit('event', {'message': f'{self.name} wanders in.'}, to=self.location)
                        break



#Class that is incapable of autonomous action. Inanimate objects and such.
class Item(Entity):
    id = itertools.count()
    def __init__(self, name, description, aliases, group, weight, location=None, equippable=False) -> None:
        super().__init__(name, description)
        self.aliases = aliases
        self.id = next(Item.id)
        self.group = group #Type of item. Food, equipment, quest, etc
        self.weight = weight #Weight of item. Will be used alongside character strength to determine if the item can be picked up, pulled to inventory, etc
        self.location = location
        self.equippable = equippable #Can you equip the item. Unsure if redundant with equipment class

#Subclass of items that is capable of being equipped by the Character class
class Equipment(Item):
    def __init__(self, name, description, group, weight, category, equippable) -> None:
        super().__init__(name, description, group, weight, equippable)
        self.equippable = True #All equipment should be equippable
        self.category = category #Further granularity with group. Is the equipment armor, clothing, sword, axe, etc. Unsure if I should instead make child classes of equipment instead so I can dictate specific stat blocks

commands.py: parses user input and calls relevant functions/methods

from flask_login import current_user
from ... import socketio
from flask_socketio import join_room, leave_room, emit
import app.blueprints.main.events as events



#List of commands that are dependant on input from the client. Still unsure as to if I need the middle man function.
@socketio.event
def client(data):
    current_player = events.world.players[session.get('player_id')]
    current_room = events.world.rooms[current_player.location]
    content = {
        'player': current_player,
        'room': current_room,
        'command': data['command'].lower(),
        'data': data['data'],
    }

    if content['command'] == 'who':
        events.world.world_who(content['player'])
        return

    elif content['command'] == 'say':
        say(content['player'], content['data'])
        return
    
    elif content['command'] in ['attack', 'kill', 'hurt', 'k', 'fight']:
        combat(content['player'], content['data'].lower())
        return

    elif content['command'][0] == '"':
        new_data = content['command'][1:]
        if ' ' in content['data']:
            data_final = f'{new_data} {content["data"]}'
            say(content['player'], data_final)
        else: say(content['player'], new_data)
        return

    elif content['command'] in ['i', 'inv', 'inv']:
        inventory_display(content['player'])
        return
    
    elif content['command'] in ['get', 'grab', 'pick', 'pickup', 'g']:
        item_get(content['player'], content['data'], content['room'])

    elif content['command'] in ['drop', 'discard', 'd']:
        item_drop(content['player'], content['data'], content['room'])

    elif content['command'] == 'whisper':
        whisper(content['player'], content['data'])
        return

    elif content['command'].lower() in ['move', 'go', 'north', 'south', 'east', 'west', 'n', 's', 'e', 'w', 'out', 'in']:
        content['data'] = content['data'].lower()
        if not content['data']:
            content['data'] = content['command'].lower()
        if content['data'] == 'n':
            content['data'] = 'north'
        if content['data'] == 's':
            content['data'] = 'south'
        if content['data'] == 'e':
            content['data'] = 'east'
        if content['data'] == 'w':
            content['data'] = 'west'
        
        move(player=content['player'], direction=content['data'], room=content['room'])
        return

    elif content['command'] == 'look' or content['command'] == 'l':
        look(player=content['player'], data=content['data'], room=content['room'])
        return

    elif content['command'] == 'test':
        test(content['player'], content['data'])

    elif content['command'] == 'save':
        save(content['player'], content['data'])
    
    elif content['command'] == '@describe':
        set_description(content['player'], content['data'])
        return

    else:
        socketio.emit('event', {'message': 'Sorry, that\'s not a command I currently understand!'}, to=content['player'].session_id)
        return


#This event should be moved to the Character class and using their move method
        
def say(player, data):
    player.speak(data)

def whisper(player, data):
    split = data.split(' ', 1)
    whisper_player = split[0]
    whisper_data = split[1]
    for world_player in events.world.players.values():
        if world_player.name == whisper_player:
            player.whisper(whisper_player=world_player, data=whisper_data)
            return
    socketio.emit('event', {'message': 'That player either doesn\'t exist, or isn\'t currently online.'}, to=player.session_id)

def combat(player, data):
    print(data)
    victim = None
    for victim_player in events.world.players.values():
        if victim_player.name.lower() == data:
            victim = victim_player
            player.combat(victim=victim)
            return
    for victim_npc in events.world.npcs.values():
        new_alias = []
        for alias in victim_npc.aliases:
            new_alias.append(alias.lower())
        if victim_npc.name.lower() == data or data in new_alias:
            victim = victim_npc
            player.combat(victim=victim)
            return
    socketio.emit('event', {'message': f'Woah there killer! I\'m not sure who you\'re trying to attack, but "{data}" certainly isn\'t here. Maybe take out your aggression on a punching bag?'}, to=player.session_id)
    
def inventory_display(player):
    player.inventory_display()

def item_get(player, data, room):
    player.item_get(data, room)

def item_drop(player, data, room):
    player.item_drop(data, room)

def move(player, direction, room):
    player.move(direction=direction, room=room)

#This event should be moved to the Player class and using their look method
def look(player, room, data=''):
    player.look(data=data, room=room)

#These are primarily test functions to make sure the world timer is functional
def test(player, data):
    pass

def save(player, player_id, sid, location, data):
    events.world.world_save()
    events.world.room_save()

def set_description(player, data):
    player.set_description(data)

sample NPC

npc_dict = {
    'Crier': {
        'name': 'Bob, the Town Crier',
        'aliases': ['Bob', 'Crier', 'The Town Crier'],
        'description': 'A young man dressed in flamboyant but common clothing, looking about as if to see who he\'ll announce the latest news to.',
        'location': '0,0',
        'home': '0,0',
        'deceased': False,
        'health': 100,
        'level': 1,
        'ambiance_list': ['The Town Crier yells "Murder in the forest! B\'ware!"', 'The Town Crier yawns and scratches himself.','You hear the Town Crier mutter "Why is it always murder? Nobody ever wants to hear about the ducks."'],
        'can_wander': True,
        'attackable': True,
    },

sample room

room_dict = {
    (0,0): {
        'name': 'Town Square',
        'description': 'This is the center of town! People flow around you, living in their own quiet worlds. You notice that nobody is making eye contact with you, and some people go out of their way to avoid you.',
        'position': '0,0',
        'exits': {
            'north': '0,1',
            'south': '0,-1',
            'east': '1,0',
            'west': '-1,0'
        },
        'icon': '<span class="map-square" style="background-color:white; color:black">()</span>',
        'ambiance_list': ['People move around you, busy about their day', 'The local town drunk stumbles by, muttering to people nobody sees.']
    },

sample item

item_dict = {
    'coin': {
        'name': 'coin',
        'aliases': ['coin', 'money',],
        'description': 'Shiny! Someone\'s going to have a bad day when they realize they dropped this. Be a thief, do it. Morality is a construct of society, and you\'ve moved beyond such petty concerns.',
        'group': 'money',
        'weight': 1,
        'location': '1,1'
    },

Frontend:

Terminal.tsx: Client-side terminal for display and input

import { useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import Map from "../Map/Map";
import Messages from "../Messages/Messages";
import CommandList from "../CommandList/CommandList";
import InventoryList from "../InventoryList/InventoryList";
import React from "react";

const Terminal = () => {

  let apiURL:string

  if (import.meta.env.MODE === 'development') {
    apiURL = 'http://localhost:5000'
  } else {
    apiURL = 'https://retromooapi.onrender.com'
  };

  let socketio = useRef(
    io(apiURL, {
      auth: localStorage.user_id,
      autoConnect: false,
    })
  );

  const [messages, setMessages] = useState([{ message: "", time: "" }]);

  const [map, setMap] = useState("");

  const [message, setMessage] = useState("");

  const [inventory, setInventory] = useState<
    { id: number; name: string; group: string }[]
  >([]);

  useEffect(() => {
    socketio.current.connect();
    return () => {
      socketio.current.disconnect();
    };
  }, []);

  useEffect(() => {
    socketio.current.on("event", (data: { message: string }) => {
      setMessages((curr) => [
        ...curr,
        { message: data.message, time: new Date().toLocaleString() },
      ]);
    });
    socketio.current.on("map", (data: { map: string }) => {
      setMap(data.map);
    });
    socketio.current.on(
      "inventory",
      (data: { inventory: { id: number; name: string; group: string }[] }) => {
        setInventory(data.inventory);
      }
    );
  }, []);

  const submitTask = (event: React.FormEvent) => {
    event.preventDefault();
    const payload = {
      command: message.substr(0, message.indexOf(" ")),
      data: message.substr(message.indexOf(" ") + 1),
    };
    if (payload.command === "" && payload.data) {
      payload.command = payload.data;
      payload.data = "";
    }
    console.log(payload);
    socketio.current.emit("client", payload);
    setMessage("");
  };

  return (
    <div className="papyrus-box">
      <div className="map-box" id="map-box">
        <Map map={map} />
      </div>
      <div className="room-commands">
        <CommandList />
      </div>
      <div className="room-inventory">
        <InventoryList inventory={inventory} />
      </div>
      <div className="room-box">
        <h2 className="room-header">Narnia</h2>
        <div className="message-box">
          <div className="messages" id="messages">
            <Messages messages={messages} />
          </div>
          <form className="input" id="input-form" onSubmit={submitTask}>
            <input
              type="text"
              name="message"
              id="message"
              placeholder="Type here!"
              required
              value={message}
              onChange={(event) => {
                setMessage(event.target.value);
              }}
            />
            <button id="send-btn" type="submit">
              Send
            </button>
          </form>
        </div>
      </div>
    </div>
  );
};
export default Terminal;
\$\endgroup\$
1
  • \$\begingroup\$ It looks like a fair bit of the React code is missing, so voting to close. \$\endgroup\$ Commented Jul 15 at 16:32

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.