From c1893200e6c9a99b25f07fa3f904cd4c3e5ade23 Mon Sep 17 00:00:00 2001 From: Manetta <> Date: Tue, 31 Jan 2023 13:50:44 +0100 Subject: [PATCH] adding an xpub mud --- xpub-mud/README.md | 154 +++++++ xpub-mud/__pycache__/mudserver.cpython-39.pyc | Bin 0 -> 7293 bytes xpub-mud/licence.md | 19 + xpub-mud/mudserver.py | 405 ++++++++++++++++++ xpub-mud/xpub_mud.py | 323 ++++++++++++++ 5 files changed, 901 insertions(+) create mode 100644 xpub-mud/README.md create mode 100644 xpub-mud/__pycache__/mudserver.cpython-39.pyc create mode 100644 xpub-mud/licence.md create mode 100644 xpub-mud/mudserver.py create mode 100644 xpub-mud/xpub_mud.py diff --git a/xpub-mud/README.md b/xpub-mud/README.md new file mode 100644 index 0000000..f63c217 --- /dev/null +++ b/xpub-mud/README.md @@ -0,0 +1,154 @@ +MUD Pi +====== + +A simple text-based Multi-User Dungeon (MUD) game, which could be run on a +Raspberry Pi or other low-end server. + + +Requirements +------------ + +You will need to install _Python_ (2.7+ or 3.3+) where you wish to run the +server. Installers for Windows and Mac can be found at +. There are also tarballs for Linux, although +the best way to install on Linux would be via the package manager. + +To allow players to connect remotely, the server will also need to be connected +to the internet. + +To connect to the server you will need a telnet client. On Mac, Linux, and +versions of Windows prior to Windows Vista, the telnet client is usually +installed by default. For Windows Vista, 7, 8 or later, you may need to follow +[this guide](http://technet.microsoft.com/en-us/library/cc771275%28v=ws.10%29.aspx) +to install it. + + +Running the Server +------------------ + +### On Windows + +Double click on `simplemud.py` - the file will be opened with the Python +interpreter. To stop the server, simply close the terminal window. + + +### On Mac OSX and Linux (including Raspberry Pi) + +From the terminal, change to the directory containing the script and run + + python simplemud.py + +Note, if you are connected to the machine via SSH, you will find that the +script stops running when you quit the SSH session. A simple way to leave the +script running is to use a tool called `screen`. Connect via SSH as usual then +run `screen`. You will enter what looks like a normal shell prompt, but now you +can start the python script running and hit `ctl+a` followed by `d` to leave +_screen_ running in the background. The next time you connect, you can +re-attach to your screen session using `screen -r`. Alternatively you could +[create a daemon script](http://jimmyg.org/blog/2010/python-daemon-init-script.html) +to run the script in the background every time the server starts. + + +Connecting to the Server +------------------------ + +If the server is running behind a NAT such as a home router, you will need to +set up port **1234** to be forwarded to the machine running the server. See your +router's instructions for how to set this up. There are a large number of +setup guides for different models of router here: + + +You will need to know the _external_ IP address of the machine running the +server. This can be discovered by visiting from +that machine. + +To connect to the server, open your operating system's terminal or command +prompt and start the telnet client by running: + + telnet 1234 + +where `` is the external IP address of the server, as described +above. 1234 is the port number that the server listens on. + +If you are using Windows Vista, 7, 8 or later and get the message: + + 'telnet' is not recognized as an internal or external command, operable + program or batch file. + +then follow +[this guide](http://technet.microsoft.com/en-us/library/cc771275%28v=ws.10%29.aspx) +to install the Windows telnet client. + +If all goes well, you should be presented with the message + + What is your name? + +To quit the telnet client, press `ctl + ]` to go to the prompt, and then +type `quit`. + + +What is Telnet? +--------------- + +Telnet is simple text-based network communication protocol that was invented in +1969 and has since been superseded by other, more secure protocols. It does +remain popular for a few specialised uses however, MUD games being one of these +uses. A long (and boring) history of the telnet protocol can be found here: + + + +What is a MUD? +-------------- + +MUD is short for Multi-User Dungeon. A MUD is a text-based online role-playing +game. MUDs were popular in the early 80s and were the precursor to the +graphical Massively-Multiplayer Online Role-Playing Games we have today, like +World of Warcraft. is a great site for learning +more about MUDs. + + +Extending the Game +------------------ + +MUD Pi is a free and open source project (that's _free_ as in _freedom_). This +means that the source code is included and you are free to read it, copy it, +extend it and use it as a starting point for your own MUD game or any other +project. See `licence.md` for more info. + +MUD Pi was written in the Python programming language. If you have never used +Python before, or are new to programming in general, why not try an online +tutorial, such as . + +There are 2 source files in the project. `mudserver.py` is a module containing +the `MudServer` class - a basic server script which handles player connections +and sending and receiving messages. `simplemud.py` is an example game using +`MudServer`, with player chat and rooms to move between. + +The best place to start tweaking the game would be to have a look at +`simplemud.py`. Why not try adding more rooms to the game world? You'll find +more ideas for things to try in the source code itself. + +Of course if you're feeling more adventurous you could take a look at the +slightly more advanced networking code in `mudserver.py`. + + +MUD-Pi-Based Projects +--------------------- + +Here are some of the cool projects people have made from MUD-Pi: + +* **[ESP8266 MUD](http://git.savsoul.com/barry/esp8266-Mud) by Barry Ruffner** - + a MUD that runs entirely within an ESP8266 microchip, using MicroPython +* **[MuddySwamp](https://github.com/ufosc/MuddySwamp) by the University of** + **Florida Open Source Club** - a UF-themed MUD +* **[Dumserver](https://github.com/wowpin/dumserver) by Bartek Radwanski** - + a feature-rich MUD engine + + +Author +------ + +MUD Pi was written by Mark Frimston + +For feedback, please email or add a comment on the +project's [Github page](http://github.com/frimkron/mud-pi) diff --git a/xpub-mud/__pycache__/mudserver.cpython-39.pyc b/xpub-mud/__pycache__/mudserver.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb0e935da2b7cfae6439019a5b002eb2a71baf2b GIT binary patch literal 7293 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 Y&s$GyzMGtTlM_h7msSu5OtPu|7dt2;d;kCd literal 0 HcmV?d00001 diff --git a/xpub-mud/licence.md b/xpub-mud/licence.md new file mode 100644 index 0000000..615dce1 --- /dev/null +++ b/xpub-mud/licence.md @@ -0,0 +1,19 @@ +Copyright (C) 2013 Mark Frimston + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/xpub-mud/mudserver.py b/xpub-mud/mudserver.py new file mode 100644 index 0000000..ba5077f --- /dev/null +++ b/xpub-mud/mudserver.py @@ -0,0 +1,405 @@ +"""Basic MUD server module for creating text-based Multi-User Dungeon +(MUD) games. + +Contains one class, MudServer, which can be instantiated to start a +server running then used to send and receive messages from players. + +author: Mark Frimston - mfrimston@gmail.com +""" + + +import socket +import select +import time +import sys + + +class MudServer(object): + """A basic server for text-based Multi-User Dungeon (MUD) games. + + Once created, the server will listen for players connecting using + Telnet. Messages can then be sent to and from multiple connected + players. + + The 'update' method should be called in a loop to keep the server + running. + """ + + # An inner class which is instantiated for each connected client to store + # info about them + + class _Client(object): + """Holds information about a connected player""" + + # the socket object used to communicate with this client + socket = None + # the ip address of this client + address = "" + # holds data send from the client until a full message is received + buffer = "" + # the last time we checked if the client was still connected + lastcheck = 0 + + def __init__(self, socket, address, buffer, lastcheck): + self.socket = socket + self.address = address + self.buffer = buffer + self.lastcheck = lastcheck + + # Used to store different types of occurences + _EVENT_NEW_PLAYER = 1 + _EVENT_PLAYER_LEFT = 2 + _EVENT_COMMAND = 3 + + # Different states we can be in while reading data from client + # See _process_sent_data function + _READ_STATE_NORMAL = 1 + _READ_STATE_COMMAND = 2 + _READ_STATE_SUBNEG = 3 + + # Command codes used by Telnet protocol + # See _process_sent_data function + _TN_INTERPRET_AS_COMMAND = 255 + _TN_ARE_YOU_THERE = 246 + _TN_WILL = 251 + _TN_WONT = 252 + _TN_DO = 253 + _TN_DONT = 254 + _TN_SUBNEGOTIATION_START = 250 + _TN_SUBNEGOTIATION_END = 240 + + # socket used to listen for new clients + _listen_socket = None + # holds info on clients. Maps client id to _Client object + _clients = {} + # counter for assigning each client a new id + _nextid = 0 + # list of occurences waiting to be handled by the code + _events = [] + # list of newly-added occurences + _new_events = [] + + def __init__(self): + """Constructs the MudServer object and starts listening for + new players. + """ + + self._clients = {} + self._nextid = 0 + self._events = [] + self._new_events = [] + + # create a new tcp socket which will be used to listen for new clients + self._listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # set a special option on the socket which allows the port to be + # immediately without having to wait + self._listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, + 1) + + # bind the socket to an ip address and port. Port 23 is the standard + # telnet port which telnet clients will use, however on some platforms + # this requires root permissions, so we use a higher arbitrary port + # number instead: 1234. Address 0.0.0.0 means that we will bind to all + # of the available network interfaces + self._listen_socket.bind(("0.0.0.0", 1234)) + + # set to non-blocking mode. This means that when we call 'accept', it + # will return immediately without waiting for a connection + self._listen_socket.setblocking(False) + + # start listening for connections on the socket + self._listen_socket.listen(1) + + def update(self): + """Checks for new players, disconnected players, and new + messages sent from players. This method must be called before + up-to-date info can be obtained from the 'get_new_players', + 'get_disconnected_players' and 'get_commands' methods. + It should be called in a loop to keep the game running. + """ + + # check for new stuff + self._check_for_new_connections() + self._check_for_disconnected() + self._check_for_messages() + + # move the new events into the main events list so that they can be + # obtained with 'get_new_players', 'get_disconnected_players' and + # 'get_commands'. The previous events are discarded + self._events = list(self._new_events) + self._new_events = [] + + def get_new_players(self): + """Returns a list containing info on any new players that have + entered the game since the last call to 'update'. Each item in + the list is a player id number. + """ + retval = [] + # go through all the events in the main list + for ev in self._events: + # if the event is a new player occurence, add the info to the list + if ev[0] == self._EVENT_NEW_PLAYER: + retval.append(ev[1]) + # return the info list + return retval + + def get_disconnected_players(self): + """Returns a list containing info on any players that have left + the game since the last call to 'update'. Each item in the list + is a player id number. + """ + retval = [] + # go through all the events in the main list + for ev in self._events: + # if the event is a player disconnect occurence, add the info to + # the list + if ev[0] == self._EVENT_PLAYER_LEFT: + retval.append(ev[1]) + # return the info list + return retval + + def get_commands(self): + """Returns a list containing any commands sent from players + since the last call to 'update'. Each item in the list is a + 3-tuple containing the id number of the sending player, a + string containing the command (i.e. the first word of what + they typed), and another string containing the text after the + command + """ + retval = [] + # go through all the events in the main list + for ev in self._events: + # if the event is a command occurence, add the info to the list + if ev[0] == self._EVENT_COMMAND: + retval.append((ev[1], ev[2], ev[3])) + # return the info list + return retval + + def send_message(self, to, message): + """Sends the text in the 'message' parameter to the player with + the id number given in the 'to' parameter. The text will be + printed out in the player's terminal. + """ + # we make sure to put a newline on the end so the client receives the + # message on its own line + self._attempt_send(to, message+"\n\r") + + def shutdown(self): + """Closes down the server, disconnecting all clients and + closing the listen socket. + """ + # for each client + for cl in self._clients.values(): + # close the socket, disconnecting the client + cl.socket.shutdown(socket.SHUT_RDWR) + cl.socket.close() + # stop listening for new clients + self._listen_socket.close() + + def _attempt_send(self, clid, data): + # python 2/3 compatability fix - convert non-unicode string to unicode + if sys.version < '3' and type(data) != unicode: + data = unicode(data, "latin1") + try: + # look up the client in the client map and use 'sendall' to send + # the message string on the socket. 'sendall' ensures that all of + # the data is sent in one go + self._clients[clid].socket.sendall(bytearray(data, "latin1")) + # KeyError will be raised if there is no client with the given id in + # the map + except KeyError: + pass + # If there is a connection problem with the client (e.g. they have + # disconnected) a socket error will be raised + except socket.error: + self._handle_disconnect(clid) + + def _check_for_new_connections(self): + + # 'select' is used to check whether there is data waiting to be read + # from the socket. We pass in 3 lists of sockets, the first being those + # to check for readability. It returns 3 lists, the first being + # the sockets that are readable. The last parameter is how long to wait + # - we pass in 0 so that it returns immediately without waiting + rlist, wlist, xlist = select.select([self._listen_socket], [], [], 0) + + # if the socket wasn't in the readable list, there's no data available, + # meaning no clients waiting to connect, and so we can exit the method + # here + if self._listen_socket not in rlist: + return + + # 'accept' returns a new socket and address info which can be used to + # communicate with the new client + joined_socket, addr = self._listen_socket.accept() + + # set non-blocking mode on the new socket. This means that 'send' and + # 'recv' will return immediately without waiting + joined_socket.setblocking(False) + + # construct a new _Client object to hold info about the newly connected + # client. Use 'nextid' as the new client's id number + self._clients[self._nextid] = MudServer._Client(joined_socket, addr[0], + "", time.time()) + + # add a new player occurence to the new events list with the player's + # id number + self._new_events.append((self._EVENT_NEW_PLAYER, self._nextid)) + + # add 1 to 'nextid' so that the next client to connect will get a + # unique id number + self._nextid += 1 + + def _check_for_disconnected(self): + + # go through all the clients + for id, cl in list(self._clients.items()): + + # if we last checked the client less than 5 seconds ago, skip this + # client and move on to the next one + if time.time() - cl.lastcheck < 5.0: + continue + + # send the client an invisible character. It doesn't actually + # matter what we send, we're really just checking that data can + # still be written to the socket. If it can't, an error will be + # raised and we'll know that the client has disconnected. + self._attempt_send(id, "\x00") + + # update the last check time + cl.lastcheck = time.time() + + def _check_for_messages(self): + + # go through all the clients + for id, cl in list(self._clients.items()): + + # we use 'select' to test whether there is data waiting to be read + # from the client socket. The function takes 3 lists of sockets, + # the first being those to test for readability. It returns 3 list + # of sockets, the first being those that are actually readable. + rlist, wlist, xlist = select.select([cl.socket], [], [], 0) + + # if the client socket wasn't in the readable list, there is no + # new data from the client - we can skip it and move on to the next + # one + if cl.socket not in rlist: + continue + + try: + # read data from the socket, using a max length of 4096 + data = cl.socket.recv(4096).decode("latin1") + + # process the data, stripping out any special Telnet commands + message = self._process_sent_data(cl, data) + + # if there was a message in the data + if message: + + # remove any spaces, tabs etc from the start and end of + # the message + message = message.strip() + + # separate the message into the command (the first word) + # and its parameters (the rest of the message) + command, params = (message.split(" ", 1) + ["", ""])[:2] + + # add a command occurence to the new events list with the + # player's id number, the command and its parameters + self._new_events.append((self._EVENT_COMMAND, id, + command.lower(), params)) + + # if there is a problem reading from the socket (e.g. the client + # has disconnected) a socket error will be raised + except socket.error: + self._handle_disconnect(id) + + def _handle_disconnect(self, clid): + + # remove the client from the clients map + del(self._clients[clid]) + + # add a 'player left' occurence to the new events list, with the + # player's id number + self._new_events.append((self._EVENT_PLAYER_LEFT, clid)) + + def _process_sent_data(self, client, data): + + # the Telnet protocol allows special command codes to be inserted into + # messages. For our very simple server we don't need to response to + # any of these codes, but we must at least detect and skip over them + # so that we don't interpret them as text data. + # More info on the Telnet protocol can be found here: + # http://pcmicro.com/netfoss/telnet.html + + # start with no message and in the normal state + message = None + state = self._READ_STATE_NORMAL + + # go through the data a character at a time + for c in data: + + # handle the character differently depending on the state we're in: + + # normal state + if state == self._READ_STATE_NORMAL: + + # if we received the special 'interpret as command' code, + # switch to 'command' state so that we handle the next + # character as a command code and not as regular text data + if ord(c) == self._TN_INTERPRET_AS_COMMAND: + state = self._READ_STATE_COMMAND + + # if we get a newline character, this is the end of the + # message. Set 'message' to the contents of the buffer and + # clear the buffer + elif c == "\n": + message = client.buffer + client.buffer = "" + + # some telnet clients send the characters as soon as the user + # types them. So if we get a backspace character, this is where + # the user has deleted a character and we should delete the + # last character from the buffer. + elif c == "\x08": + client.buffer = client.buffer[:-1] + + # otherwise it's just a regular character - add it to the + # buffer where we're building up the received message + else: + client.buffer += c + + # command state + elif state == self._READ_STATE_COMMAND: + + # the special 'start of subnegotiation' command code indicates + # that the following characters are a list of options until + # we're told otherwise. We switch into 'subnegotiation' state + # to handle this + if ord(c) == self._TN_SUBNEGOTIATION_START: + state = self._READ_STATE_SUBNEG + + # if the command code is one of the 'will', 'wont', 'do' or + # 'dont' commands, the following character will be an option + # code so we must remain in the 'command' state + elif ord(c) in (self._TN_WILL, self._TN_WONT, self._TN_DO, + self._TN_DONT): + state = self._READ_STATE_COMMAND + + # for all other command codes, there is no accompanying data so + # we can return to 'normal' state. + else: + state = self._READ_STATE_NORMAL + + # subnegotiation state + elif state == self._READ_STATE_SUBNEG: + + # if we reach an 'end of subnegotiation' command, this ends the + # list of options and we can return to 'normal' state. + # Otherwise we must remain in this state + if ord(c) == self._TN_SUBNEGOTIATION_END: + state = self._READ_STATE_NORMAL + + # return the contents of 'message' which is either a string or None + return message diff --git a/xpub-mud/xpub_mud.py b/xpub-mud/xpub_mud.py new file mode 100644 index 0000000..80d1e58 --- /dev/null +++ b/xpub-mud/xpub_mud.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python + +"""A simple Multi-User Dungeon (MUD) game. Players can talk to each +other, examine their surroundings and move between rooms. + +Some ideas for things to try adding: + * More rooms to explore + * An 'emote' command e.g. 'emote laughs out loud' -> 'Mark laughs + out loud' + * A 'whisper' command for talking to individual players + * A 'shout' command for yelling to players in all rooms + * Items to look at in rooms e.g. 'look fireplace' -> 'You see a + roaring, glowing fire' + * Items to pick up e.g. 'take rock' -> 'You pick up the rock' + * Monsters to fight + * Loot to collect + * Saving players accounts between sessions + * A password login + * A shop from which to buy items + +author: Mark Frimston - mfrimston@gmail.com +""" + +import time + +# import the MUD server class +from mudserver import MudServer + + +# structure defining the rooms in the game. Try adding more rooms to the game! +rooms = { + "studio": { + "description": "You're in the XPUB studio. Someone is making coffee in the back.", + "exits": {"south": "neutral zone"} + }, + "office": { + "description": "You're in the office. Leslie is on the phone.", + "exits": {"north": "neutral zone"} + }, + "neutral zone": { + "description": "You're in the neutral zone. Some stuff happens.", + "exits": { + "south": "office", + "north": "studio", + "west" : "artificial research station" + } + }, + "artificial research station": { + "description": "You're in the artificial research station. What happens here?", + "exits": {"south": "neutral zone"} + } +} + +# stores the players in the game +players = {} + +# start the server +mud = MudServer() + +# main game loop. We loop forever (i.e. until the program is terminated) +while True: + + # pause for 1/5 of a second on each loop, so that we don't constantly + # use 100% CPU time + time.sleep(0.2) + + # 'update' must be called in the loop to keep the game running and give + # us up-to-date information + mud.update() + + # go through any newly connected players + for id in mud.get_new_players(): + + # add the new player to the dictionary, noting that they've not been + # named yet. + # The dictionary key is the player's id number. We set their room to + # None initially until they have entered a name + # Try adding more player stats - level, gold, inventory, etc + players[id] = { + "name": None, + "room": None, + } + + # send the new player a prompt for their name + mud.send_message(id, "What is your name?") + + # go through any recently disconnected players + for id in mud.get_disconnected_players(): + + # if for any reason the player isn't in the player map, skip them and + # move on to the next one + if id not in players: + continue + + # go through all the players in the game + for pid, pl in players.items(): + # send each player a message to tell them about the diconnected + # player + mud.send_message(pid, "{} quit the game".format(players[id]["name"])) + + # remove the player's entry in the player dictionary + del(players[id]) + + # go through any new commands sent from players + for id, command, params in mud.get_commands(): + + # if for any reason the player isn't in the player map, skip them and + # move on to the next one + if id not in players: + continue + + # if the player hasn't given their name yet, use this first command as + # their name and move them to the starting room. + if players[id]["name"] is None: + + players[id]["name"] = command + # this is the room in which the game starts + players[id]["room"] = "studio" + + # go through all the players in the game + for pid, pl in players.items(): + # send each player a message to tell them about the new player + mud.send_message(pid, "{} entered the game".format( + players[id]["name"])) + + # send the new player a welcome message + mud.send_message(id, "Welcome to the game, {}. ".format( + players[id]["name"]) + + "Type 'help' for a list of commands. Have fun!\n") + + # send the new player the description of their current room + mud.send_message(id, rooms[players[id]["room"]]["description"]) + + # each of the possible commands is handled below. Try adding new + # commands to the game! + + # 'help' command + elif command == "help": + + # send the player back the list of possible commands + mud.send_message(id, "Commands:") + mud.send_message(id, " say - Says something out loud, " + + "e.g. 'say Hello'") + mud.send_message(id, " look - Examines the " + + "surroundings, e.g. 'look'") + mud.send_message(id, " go - Moves through the exit " + + "specified, e.g. 'go outside'") + mud.send_message(id, " create - Creates a new exit and room") + mud.send_message(id, " describe - Change the description of the current room") + + # 'say' command + elif command == "say": + + # go through every player in the game + for pid, pl in players.items(): + # if they're in the same room as the player + if players[pid]["room"] == players[id]["room"]: + # send them a message telling them what the player said + mud.send_message(pid, "{} says: {}".format( + players[id]["name"], params)) + + # 'look' command + elif command == "look": + + # store the player's current room + current_room = rooms[players[id]["room"]] + + # send the player back the description of their current room + mud.send_message(id, current_room["description"]) + + playershere = [] + # go through every player in the game + for pid, pl in players.items(): + # if they're in the same room as the player + if players[pid]["room"] == players[id]["room"]: + # ... and they have a name to be shown + if players[pid]["name"] is not None: + # add their name to the list + playershere.append(players[pid]["name"]) + + # send player a message containing the list of players in the room + playershere_string = ", ".join(playershere) + mud.send_message(id, f"Players here: { playershere_string }") + + # send player a message containing the list of exits from this room + exits = ", ".join(current_room["exits"]) + mud.send_message(id, f"Exits are: { exits }") + + # 'go' command + elif command == "go": + + # store the exit name + ex = params.lower() + + # store the player's current room + current_room = rooms[players[id]["room"]] + + # if the specified exit is found in the room's exits list + if ex in current_room["exits"]: + + # go through all the players in the game + for pid, pl in players.items(): + # if player is in the same room and isn't the player + # sending the command + if players[pid]["room"] == players[id]["room"] and pid != id: + # send them a message telling them that the player + # left the room + mud.send_message(pid, "{} left via exit '{}'".format(players[id]["name"], ex)) + + # update the player's current room to the one the exit leads to + players[id]["room"] = current_room["exits"][ex] + current_room = rooms[players[id]["room"]] + + # go through all the players in the game + for pid, pl in players.items(): + # if player is in the same (new) room and isn't the player + # sending the command + if players[pid]["room"] == players[id]["room"] \ + and pid != id: + # send them a message telling them that the player + # entered the room + mud.send_message(pid, + "{} arrived via exit '{}'".format( + players[id]["name"], ex)) + + # send the player a message telling them where they are now + mud.send_message(id, "You arrive at '{}'".format( + players[id]["room"])) + + # the specified exit wasn't found in the current room + else: + # send back an 'unknown exit' message + mud.send_message(id, "Unknown exit '{}'".format(ex)) + + # 'create' command + elif command == "create": + + # store the exit or room that will be created + parameters = params.lower() + parameters_list = parameters.split() + print("[INSPECT] parameters: ", parameters_list) + + if len(parameters_list) >= 1: + # store the new exit name + new_exit = parameters_list[0] + print("[INSPECT] new exit: ", new_exit) + else: + new_exit = None + + if len(parameters_list) >= 2: + # store the new room name + new_room = " ".join(parameters_list[1:]) + print("[INSPECT] new room: ", new_room) + else: + new_exit = None + + # store the player's current room + current_room = players[id]["room"] + print("[INSPECT] current room: ", current_room) + + # store information about the player's current room + current_room_dict = rooms[players[id]["room"]] + print("[INSPECT] current room dict: ", current_room_dict) + + # if both the new exit and new room are given + if new_exit is not None and new_room is not None: + + # send player a message when the exit already exists + if new_exit in current_room_dict["exits"]: + mud.send_message(id, "This exit already exist.") + exits = ", ".join(current_room["exits"]) + + # create new room + else: + print(f"[INSPECT] Make new room: { new_room }, in the direction: { new_exit }") + + # add the new exit to the current room + rooms[current_room]["exits"][new_exit] = new_room + + # store information about the new room + rooms[new_room] = {} + rooms[new_room]["description"] = "" + rooms[new_room]["exits"] = {} + + # add the opposite exit direction to the exits of the new room + if new_exit == "west": + exit_to_add = "east" + elif new_exit == "east": + exit_to_add = "east" + if new_exit == "north": + exit_to_add = "south" + elif new_exit == "south": + exit_to_add = "north" + + # store this exit to the new room + rooms[new_room]["exits"][exit_to_add] = current_room + + # announce the new room to the player + mud.send_message(id, f"A new room is added: { new_room } (in the { new_exit })") + # invite the player to write a description for the room + mud.send_message(id, "The room is not described yet. When you are in the room, you can use 'describe' to add a description. For example: 'describe This is the XML! It smells a bit muffy here.'") + + # warn the player when the "create" command is not used in the right way + else: + mud.send_message(id, f"Sorry you cannot create a new room in that way. Try: 'create direction roomname'") + + # 'describe' command + elif command == "describe": + + # store the exit or room that will be created + description = params.lower() + print("[INSPECT] description: ", description) + + # store the player's current room + current_room = players[id]["room"] + print("[INSPECT] current room: ", current_room) + + rooms[new_room]["description"] = description + + # some other, unrecognised command + else: + # send back an 'unknown command' message + mud.send_message(id, "Unknown command '{}'".format(command))