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