adding an xpub mud
parent
50df7aab12
commit
c1893200e6
@ -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
|
||||||
|
<http://www.python.org/download/>. 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:
|
||||||
|
<http://portforward.com/english/routers/port_forwarding/>
|
||||||
|
|
||||||
|
You will need to know the _external_ IP address of the machine running the
|
||||||
|
server. This can be discovered by visiting <http://www.whatsmyip.org> 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 <ip address> 1234
|
||||||
|
|
||||||
|
where `<ip address>` 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:
|
||||||
|
<http://www.cs.utexas.edu/users/chris/think/ARPANET/Telnet/Telnet.shtml>
|
||||||
|
|
||||||
|
|
||||||
|
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. <http://www.mudconnect.com> 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 <http://www.learnpython.org/>.
|
||||||
|
|
||||||
|
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 <mfrimston@gmail.com> or add a comment on the
|
||||||
|
project's [Github page](http://github.com/frimkron/mud-pi)
|
Binary file not shown.
@ -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.
|
@ -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
|
@ -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 <message> - Says something out loud, "
|
||||||
|
+ "e.g. 'say Hello'")
|
||||||
|
mud.send_message(id, " look - Examines the "
|
||||||
|
+ "surroundings, e.g. 'look'")
|
||||||
|
mud.send_message(id, " go <exit> - Moves through the exit "
|
||||||
|
+ "specified, e.g. 'go outside'")
|
||||||
|
mud.send_message(id, " create <exit> <new roomname> - Creates a new exit and room")
|
||||||
|
mud.send_message(id, " describe <description> - 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))
|
Loading…
Reference in New Issue