Git Product home page Git Product logo

swampymud's Introduction

swampymud

Multi-user dungeons, or "MUDs" are text-based role-playing games, that naturally evolved from the text-based rpg and adventure games of the 1970s. This project aims to introduce a new generation—one that never experienced a world without broadband internet—to this classic game genre. While this code can be adapted for any setting, we intend to render our university in beautiful ASCII.

(Note: this project should be considered an alpha. Expect changes to the API.)

Requirements

Hosting

For hosting a server, Python 3 must be installed on the system (along with an appropriate internet connection.) For help with Python installation, visit https://www.python.org.

This project also requires the packages websockets (>= 8.1) and PyYAML (>=5.3.0).

Connecting

For connecting to an existing server, a simple telnet client is required. However, we recommend using a dedicated MUD client to avoid ugliness like this:

raw_telnet.png

There are many solid MUD clients available. We have been using Mudlet, a completely free and open source MUD client. Here's the same scenario, in Mudlet:

mudlet_client.png

You can now connect to a swampymud in your browser using a websocket. More on this later...

Getting Started

Hosting

I recommend installing this package using pip:

pip3 install swampymud

Alternatively, if you want to install from source, you can clone this repo and install the requirements with pip3 install -r requirements.txt.

Either way, you can launch a Swampy MUD server right away like so:

python3 -m swampymud

By default, this will start a WebSocket server on port 9000. If you want to specify a different port (e.g. 4000), you can run

python3 -m swampymud --ws 4000

To start a plain TCP server instead of a WebSocket server, use the --tcp flag.

python3 -m swampymud --tcp 8333

Why not both? You can provide both the --ws and --tcp arguments to start both a WebSocket server and a TCP server.

# this will start a WebSocket server on port 1234
# AND a plain TCP server on port 5678
python3 -m swampymud --ws 1234 --tcp 5678

For a full list of options, run python3 -m swampymud --help.

If you are hosting a server for other people to connect, you will need to port foward your router. When you port forward, select the TCP protocol and direct traffic towards whatever port the server is listening on.

Connecting

with a Raw Telnet Client

If you want to use an ugly, raw telent client, you can use the following terminal command on *nix systems:

# telnet <ip address> <port>
# if you started a server with the default settings:
telnet localhost 9000

On Windows, a telnet client is not provided by default. One option is to follow this guide to enable the Windows telnet client.

Alternatively, you can install PuTTY, a free and open source telnet and ssh client.

with Mudlet

  1. Run Mudlet. You will be prompted to "Select a profile to connect with".
  2. You must enter a set of required fields:
    • For "Profile name", put whatever you prefer.
    • For "Server address", put the address of the server.
    • For "Port", put the port of the server.
  3. Once all the fields are entered, simply press "connect".
  4. Have fun!
  5. When you exit Mudlet, you will be asked if you want to save the profile. Select "Yes", and simply load the profile next time you play.

Contributing

Please read CONTRIBUTING.md for how to work on the project.

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

swampymud's People

Contributors

adamvanbelkum avatar awhigham9 avatar chadlung avatar chlolol avatar davidiswhat avatar frimkron avatar gageowe avatar ganon1998 avatar hjarrell avatar jordanwhittle avatar pankeleo avatar wsowens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swampymud's Issues

Add a recursive 'find' method

On the branch overhaul-import, as of commit 12261d6, we have a working World class. Previously, we used a Library class that attempted to track each game object (items, chars, entities) in individual classes. Unlike its predecessor, the world World will only keep track of Locations, nothing more.

This means that we should add a method find to the World class that recursively searches through each location.

This find should be modeled on the *nix find utility, and allow for multiple parameters, including name (in-game string representation), symbol (external symbol used for object), and type (character, entity, item, or more specific subclass.)

For instance, world.find(name="Bill", type=Wizard) should return a list of all objects in the world that have a class "Wizard" and the name "Bill". If no objects match the parameter, the method will return an empty list. Similarly, world.find(type=Warrior) should return a list of all Warriors in the world.

Beyond implementing the method for the World class, this method should also be implemented for Locations, Characters, Entities, and Items. The find method can then recurse and look through each of the characters in a location, items in a character, and so on.

This method can then easily be adapted into a server administrator command.

Humanoid CharacterClass needed

Character (defined in character.py) is a base class for ALL other characters. It implements the basic traits of all characters, including default commands (walk, say, etc). The Character base class is general purpose: no mana, no health, no specialized commands, etc.

We need to create a new class, Humanoid, that will serve as a base class for all of our humanoid CharacterClasses (Wizard, Healer, Knight, etc.). This class should include attributes we expected to be common to all humanoid CharacterClasses. For instance, all humanoid classes will have a concept of "health", etc.

Any discussion is welcome in the comments!

Update simplemud.py using Location class

simplemud.py currently uses a dictionary to represent locations. Let's fix that, and use the new Location class, defined in location.py

Differences in user experience should be minimal.

Should we change cmd_walk to be cmd_go?

Do we want to make assumptions about characters' modes of locomotion? Typing walk could break immersion for anyone playing as a slug person.

To be more pragmatic: having to type 2 fewer characters could add up tremendously over time.

Update Contributing.md

Under the "creating a location" section, we should include an example location json (with all proper formatting). This should also include instructions on how to use the whitelist / blacklist features.

Bug: [ERROR] Unhandled exception in client_connected_cb

C:\Users\default>python -m swampymud --tcp 1234
2024-03-01 14:43:55,072 [INFO] Launching a TCP Server on port '1234'
2024-03-01 14:43:55,074 [WARNING] C:\Users\default\AppData\Local\Programs\Python\Python312\Lib\site-packages\swampymud\__main__.py:121: DeprecationWarning: There is no current event loop
  asyncio.get_event_loop().run_until_complete(server.run())

2024-03-01 14:44:07,881 [INFO] 0 joined.
2024-03-01 14:44:07,882 [WARNING] Default Character has no default location, so 0 will be spawned in Swampy Tavern
2024-03-01 14:44:07,882 [ERROR] Unhandled exception in client_connected_cb
transport: <_ProactorSocketTransport fd=384 read=<_OverlappedFuture pending cb=[_ProactorReadPipeTransport._loop_reading()]>>
Traceback (most recent call last):
  File "C:\Users\default\AppData\Local\Programs\Python\Python312\Lib\site-packages\swampymud\mudserver.py", line 142, in _register_tcp
    await asyncio.wait([self._incoming_tcp(pid, reader),
  File "C:\Users\default\AppData\Local\Programs\Python\Python312\Lib\asyncio\tasks.py", line 461, in wait
    raise TypeError("Passing coroutines is forbidden, use tasks explicitly.")
TypeError: Passing coroutines is forbidden, use tasks explicitly.
2024-03-01 14:44:07,885 [WARNING] C:\Users\default\AppData\Local\Programs\Python\Python312\Lib\asyncio\streams.py:281: RuntimeWarning: coroutine 'MudServer._incoming_tcp' was never awaited
  self._task = None

2024-03-01 14:44:07,886 [WARNING] C:\Users\default\AppData\Local\Programs\Python\Python312\Lib\asyncio\streams.py:281: RuntimeWarning: coroutine 'MudServer._outgoing_tcp' was never awaited
  self._task = None

Here I've copied the output directly from the command line. This is a brand new installation of Python 3.12 on Windows 11 and all the required modules are fully updated. The Telnet client I'm using to connect is Mudlet and here's what it looks like on that end:
image

To be clear, the server seems to run fine in the background, but throws all these errors as soon as I try to join it. Not totally sure what the problem is or how to fix it.

Add describe() methods to all of the classes in the sandbox

Our sandbox is composed of entities, items, characters, and locations.
Each of these classes needs a flexible, describe() method.

For reference, we have agreed upon the following convention:

class Foo:
    _name               # private variable holding the name

    def __repr__(self): # returns a proper, pythonic representation

    def __str__(self):  # returns the name

    def describe(self, *args):     # returns the a thorough description of the thing

info() should allow for some structured input and return appropriate output.
For example, given Wizard bill:

>>> bill.info("wielding")
"wielding an enchanted staff"
>>> bill.info("epithet")
"the Wizard"
>>> bill.info("epithet", "wielding")
("the Wizard", "wielding an enchanted staff")

This isn't set in stone, so other ideas are definitely welcome.

We can probably implement this by having some kind of dictionary.

Update cmd_look in character.py to also list players in current location

Currently, typing look yields the current locations' name and description, but does not include other characters in the current location.

For instance:

> look
Hoggetown Pub and Inn:
A warm fire crackles in the corner. The countertop is dotted with stains of ale--a good sign.

Ideally, if we have another player, Richard the Wizard, we should see something like this:

> look
Hoggetown Pub and Inn:
A warm fire crackles in the corner. The countertop is dotted with stains of ale--a good sign.

You see Richard the Wizard.

If we had both Richard the Wizard and Steve the Thief:

> look
Hoggetown Pub and Inn:
A warm fire crackles in the corner. The countertop is dotted with stains of ale--a good sign.

You see Richard the Wizard and Steve the Thief.

And finally. if you had Richard the Wizard, Steve the Thief, and Linus the Knight:

> look
Hoggetown Pub and Inn:
A warm fire crackles in the corner. The countertop is dotted with stains of ale--a good sign.

You see Richard the Wizard, Steve the Thief, and Linus the Knight.

To accomplish this, you'll need to modify the cmd_look method of character.py. There is a relevant method in the location class, which is defined in location.py.

Add a Color module

As we move towards prettier output, we need to add a Color module to the util package that allows that allows us to control color in a high level way. These high-level descriptions of color can then be implemented in varying ways depending on the client.

For instance, it might look something like this:

class Colored:
    def __init__(self, text, fg, bg)

   def __repr__(self) # textual representation 

   def __str__(self) # only get the message (no color)

   def ansi() # get a representation using ANSI color codes

With this, we have easy and portable colors that we can use for both the server admin and client.

Implement cmd_drop in the Character class

Since we updated develop, players can now pick up items in Locations. We want players to be able to drop items in their inventory.

As such, need a cmd_drop method similar to the cmd_pickup method.

Convert all commands to new-style commands

Currently, there is both a mix of old-style and new-style commands in throughout the code base, with old-style commands predominating. The new style of writing commands is more reusable, modular, and intuitive.

An old-style command is written as follows, within the Character class or one of its subclasses:

def cmd_foo(self, *other_params*, args):
    # Do something

All old style commands' method signatures begin with "cmd_". This is required because the command dictionary (for a Character instance, self.cmd_dict()) is written so that methods beginning with "cmd_" are added the users dictionary under the name of the substring after "cmd_" in the method signature. Then the user would call it by typing this substring to the console. For example, for the above command a user would type "foo" followed by any necessary arguments.

New-style commands, instead, use decorators to accomplish a similar class. The previous command written in the new style might look like:

import command

class someCharacterClass:
    # Other Functions

   @command.class_command
    def foo(self, *other_params*, args):
        # Do something

The decorator @command.character_command is invoking a hypothetical function in command.py. This might look like:

def class_command(func):
    return Command(func.__name__, func, "Class", )

For more examples of writing new-style commands, see entity.py and entitytest.py.

All commands should be converted to this style. This will require creating all decorators and the corresponding higher order function calls, as well as all necessary new classes for specific command types.

Relevant files: command.py, character.py, subclasses of character.py

Update MuddySwamp.py to add more Administrator commands

For those unfamiliar, when you run MuddySwamp.py, the script logs all activity, and also accepts input from the person running the script. As the administrator (person running the script), you can enter commands.

Right now we have:

broadcast [message] : send message to all players
players: list players
help: print help menu

Possible commands include:

  • kick: force a player to disconnect (may require changing the mudserver.py code)
  • whisper [player] [message]: tell [player] a [message]
  • move [player] [location name]: forcefully move a player to another location
  • give [player] [item]: give a [player] an [item]

I'd love to hear any more ideas for Administrator commands!

Update cmd_look

By default, the player receives no information when they walk in a room.

This is undesirable for a few reasons. First, we want users to know the contents of a room the moment they walk in. However, we don't want to blast users with a location's description every time they walk in.

Here is an ideal outcome:

> walk bathroom
You see Bill the Dark Wizard and Leo the Dark Wizard.
You see a sword on the ground.
Exits Available: exit: Marston Basement, stall: Basement Stall

> look
A paper towel dispenser precariously hangs from the wall. You notice one stall has a strange green
glow emanating from it.

You see Bill the Dark Wizard and Leo the Dark Wizard.
You see a sword on the ground.
Exits Available: exit: Marston Basement, stall: Basement Stall

This means less typing for our users. Moreover, we can eventually let look take arguments:

> look sword
Rusty Sword: Damage 10, Value 3
and if we add entities to the game:
> look paper towel dispenser
The paper towel dispenser appears to have something inside.

Implementation details are not as important here, but the function that builds the "current stats" for the location...

You see Bill the Dark Wizard and Leo the Dark Wizard.
You see a sword on the ground.
Exits Available: exit: Marston Basement, stall: Basement Stall

...should be located in location.py. Feel free to modify __str__ in the location class.

When a player calls look, they get the location's description and "current stats". When I player enters a location, they should only get the "current stats".

Add logging ability to server

We would like to make a log file of all events happening in the server. Whether this be users connecting, what they say, and any other server events. This would make it a lot easier to track down any issues with the games and problems with what people say.

Rewrite the EquipTarget class to less memory intensive

The current EquipTarget class returns redundant objects. We should simply use a static method to get or create an EquipTarget.

from item.py:

class EquipTarget:
    next_id = 0
    _target_list = []

    def __init__(self, name):
        if name.lower() in self._target_list:
            self.target_id = self._target_list.index(name.lower())
        else:
            self.target_id = EquipTarget.next_id
            EquipTarget.next_id += 1
            self._target_list.append(name.lower())
        self.name = name

    def __str__(self):
        return self.name 

    def __eq__(self, other):
        try:
            return self.target_id == other.target_id
        except AttributeError:
            # other item is not an EquipTarget
            return False
    
    def __hash__(self):
        return self.target_id
    
    def __repr__(self):
        return str(self) + "[%s]" % self.target_id 

    @staticmethod
    def make_dict(*names):
        #TODO: make support for default items?
        equip_dict = {}
        for name in names:
            equip_dict[EquipTarget(name)] = None
        return equip_dict

Broadcast command no longer working

The server administrator command broadcast is no longer working. I suspect this is because of the new way players are managed. Ideally, broadcast should send the provided message to all Receivers on the server.

As you can see from the client (right window), no message is received.
issue

Implementation is not a concern here.

Confirmation of successful command inputs using debug mode

I feel like it would provide more clarity to the project if it simply printed out a "Success!" or, "command not found" kind of line whenever a server command is made, if a "debug mode" is set to true. If there is a bug on the character side, (like with the broadcasting issue), and new people are messing with the program, then they know for sure that the command executed.

Decide on an Item abstract data type

What should items look like? How should they behave?

We want items to be as flexible as possible, assuming only that they know their user and their target (possibly the same). Possible subdivisions of items might be:

Equipables: tools, apparel, etc.
Consumables: potions, food, etc.
(Other categories?)

I really have no idea who we should do this, so any suggestions are more than welcome.

fix the StocString class

StocStrings are not currently processed correctly at all.

A stocstring allows us to add a bit of RNG into our JSONs. Here is an example:

{
    "name": "Treasure Room",
    "items": {
        "Health Potion": !{choice(2,3,4)}
    }
}

When this json is processed (as a string) with the StocString.process() method, we should expect something like this:

{
    "name": "Treasure Room",
    "items": {
        "Health Potion": 2
    }
}

The quantity for "Health Potion" has an equal chance of being 2, 3, or 4. The !{....} is executed as inline Python code. (Note that we use Python's random module.)

Right now the parser is incorrect. It uses a regex, when really we need something like a stack. The parser must be able to track the open and close braces.

Scripted dialogue format needed

We need a robust format for an executable dialog format. The key features it should support are:

  • messaging blurbs of text to the player
  • listing choices to the player, and waiting for a reply
  • linking actions to specific choices; ex:
    • unlock door
    • spawn dragon
    • increase player's health
    • give the player an item
  • making some choices only offered under certain conditions, ex:
    • if the player is a certain class
    • if the player is in a faction
    • if a dragon is already in the current location
    • if this player has already selected this dialog option
  • making some responses / actions occur under certain conditions

Moreover, this format should emphasize:

  • modularity
  • readability
  • ease of writing

We want people who aren't necessarily familiar with the deeper parts of the engine, to still be able to write scripted dialogue interactions. That said, it would be nice if more advanced users could plug direct engine code (python) into a dialog script (if desired).

Remove any tabs used in user-directed output

Refer to the image below:

image

The help menu looks atrocious because this client does not render tabs as expected.
We should not include the tab character in user-directed outputs so that we may ensure a consistent experience for the users.

We should set a standard (2 spaces? 4 spaces?) that we follow for any client-side indentation. We could also make a "TAB" global in character.py and use that.

Discussion is welcome below.

Use the "@property" decorator to make certain attributes readonly

Because people writing scripts for the game may be unaware the engine's mechanics and Python style, we should make certain properties "read only". We have the following methods in location.py

In class Exit

def get_destination(self):
    return self._destination

In class Location:

def get_character_list(self):
       #return a copy of the list so that users can't accidentally mess it up
        return list(self._character_list)

These un-Pythonic getter methods should be changed to simple properties using the @Property decorator. Ideally, I want to be able to do:
location1.character_list and receive a copy of the list of characters.

Here's a random article that gets the point across:
https://www.programiz.com/python-programming/property

Disclaimer: Python follows the "we're all adults here" principle. So, if we make a method "private", users should not use it. However, as stated above, this simple fix would make our code more Pythonic while still avoiding possible issues down the line.

Fix "Command Not Recognized" Error from blank/whitespace line.

For the person running MuddySwamp.py, pressing enter/return submits a blank line to be parsed as an admistrator command. We want to avoid that, since printing that error message is annoying. Any line composed of nothing but whitespace should just be ignored (no error message).

Example:

2018-09-19 19:37:43,587 [MainThread] [INFO] Server commands are: 
 broadcast [message] - Broadcasts a message to the entire server
 players - Prints a list of all players
 stop - Stops the server
  
2018-09-19 19:37:45,729 [MainThread] [INFO] Command not recognized. Type help for a list of commands.

2018-09-19 19:37:46,088 [MainThread] [INFO] Command not recognized. Type help for a list of commands.
   
2018-09-19 19:37:48,932 [MainThread] [INFO] Command not recognized. Type help for a list of commands.

Add Class checking to items

Players should be restricted from using certain items based on their class. For instance, a warrior should not be able to equip a mage's staff, or use a magic scroll. As such, we should add a whitelist and blacklist field to each item. Please refer to the system used for exits as an example.

Write unit tests for each module

To encourage better software development practices, we should write unit tests for all of the major engine-related modules. This will elevate our testing protocol above just smoke testing.

I'll provide an example for the location module in a moment.

Edit:
The documentation for the unittest module is quite thorough. I highly recommend giving it a read.

Add a login system that allows users to regain control of a character after logging out

Currently, players have no means of "logging in" to an account. We need a system that looks like this:

Welcome to MuddySwamp?
Do you have an existing character? (yes/no) yes
Great! Enter the password for that character:
**********
You have loaded your character, Bill the Wizard.

Or:

Welcome to MuddySwamp?
Do you have an existing character? (yes/no) n
Creating a character...
You are a Healer.
Enter a password to recover this account later:
*********
What is your name?
Matt
Welcome, Matt the Healer!

Implement class checking for Exits in Character.py

The Exit class in location.py has two CharFilter fields, access and visibility. These CharFilter fields should dictate when a character is allowed to see that an exit exists (with the look command) and when a character is allowed to actually traverse an exit.

@pankeleo and believe that a character should still be able to enter an exit even if they cannot see it with the look command.

Refer to the CharFilter class in character.py for information on how to use the CharFilter.

Add a "CharacterFilter" class

Right now, the Exit class has a lot of specialized code to deal with allowing certain CharacterClasses in, and keeping certain CharacterClasses out.

In the interest of abstraction, we should abstract this code out into a new class, CharacterFilter. We could then painlessly reuse this functionality for items and entities later. This would solve Issue #27 as well.

Update CharacterParser to fit new json format

We have now added a "start" field to the CharacterClass json. See ExampleClass.json as an example:

{
    "name" : "ExampleClass",
    "frequency" : 1,
    "start" : "Turlington Plaza",
    "path" : "scripts/ExampleClass.py"
}

The CharacterParser should attempt to grab this field, and create a Dependency to be resolved based on this field. Resolve function might look like:

def resolve(class=imported_class, location_name=json_data["start"]):
    class.start = library[Location][location_name]

Refer to the dependency creation process in LocationParser for more details.

Switch mudimport to use YAML

We can easily switch our game files from JSON to YAML, since YAML is a superset of JSON.
This will allow for faster development and 10% less time spent typing braces.

Fix multiple players disconnecting at once

2018-06-21 01:01:28,469 [root] [INFO] Player 7 left
Traceback (most recent call last):
  File "MuddySwamp.py", line 194, in <module>
    mud.send_message_to_all("%s quit the game" % players[event.id]["name"])
  File "/home/pi/MUD/MuddySwamp/mudserver.py", line 222, in send_message_to_all
    for client in self._clients:
RuntimeError: dictionary changed size during iteration

Add more testing locations to locations/

Locations should follow the .json format described in locations/template.txt

These locations are not intended to be final, and as the setting changes, we probably delete some and update others. We mostly just need to rigorously test the Location navigation and parsing system.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.