Initial commit
0 parents
Showing
11 changed files
with
1037 additions
and
0 deletions
commands.py
0 → 100644
1 | |||
2 | class CommandHandler(object): | ||
3 | |||
4 | def tokenize_params(self): | ||
5 | cleaned = self.params.lower().strip() | ||
6 | tokens = [] | ||
7 | for token in cleaned.split(' '): | ||
8 | token = token.strip() | ||
9 | if len(token) > 0: | ||
10 | tokens.append(token) | ||
11 | self.tokens = tokens | ||
12 | |||
13 | def look(self): | ||
14 | self.mud.send_message(self.id, "") | ||
15 | if len(self.tokens) > 0 and self.tokens[0] == 'at': | ||
16 | del self.tokens[0] | ||
17 | if len(self.tokens) == 0: | ||
18 | self.mud.send_message(self.id, self.room.get_description()) | ||
19 | else: | ||
20 | subject = self.tokens[0] | ||
21 | items = self.room.get_look_items() | ||
22 | for item in items: | ||
23 | if subject in item: | ||
24 | self.mud.send_message(self.id, items[item]) | ||
25 | return True | ||
26 | self.mud.send_message(self.id, "That doesn't seem to be here.") | ||
27 | |||
28 | playershere = [] | ||
29 | # go through every player in the game | ||
30 | for pid, pl in self.players.items(): | ||
31 | # if they're in the same room as the player | ||
32 | if self.players[pid]["room"] == self.players[self.id]["room"]: | ||
33 | # ... and they have a name to be shown | ||
34 | if self.players[pid]["name"] is not None: | ||
35 | # add their name to the list | ||
36 | playershere.append(self.players[pid]["name"]) | ||
37 | |||
38 | # send player a message containing the list of players in the room | ||
39 | self.mud.send_message(self.id, "Players here: {}".format( | ||
40 | ", ".join(playershere))) | ||
41 | |||
42 | # send player a message containing the list of exits from this room | ||
43 | self.mud.send_message(self.id, "Exits are: {}".format( | ||
44 | ", ".join(self.room.get_exits()))) | ||
45 | |||
46 | return True | ||
47 | |||
48 | def parse(self, id, cmd, params, room, mud, players): | ||
49 | self.id = id | ||
50 | self.cmd = cmd | ||
51 | self.params = params | ||
52 | self.tokenize_params() | ||
53 | self.room = room | ||
54 | self.mud = mud | ||
55 | self.players = players | ||
56 | |||
57 | try: | ||
58 | method = getattr(self, cmd) | ||
59 | return method() | ||
60 | except AttributeError: | ||
61 | return False | ||
62 |
help/help.txt
0 → 100644
1 | Commands: | ||
2 | say <message> - Says something out loud, e.g. 'say Hello' | ||
3 | look - Examines the surroundings, e.g. 'look' | ||
4 | go <exit> - Moves through the exit specified, e.g. 'go outside' | ||
5 | save - Saves your character | ||
6 | quit - Saves your character then closes the connection | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
main.py
0 → 100644
1 | #!/usr/bin/env python | ||
2 | |||
3 | """A simple Multi-User Dungeon (MUD) game. Players can talk to each | ||
4 | other, examine their surroundings and move between rooms. | ||
5 | |||
6 | Some ideas for things to try adding: | ||
7 | * More rooms to explore | ||
8 | * An 'emote' command e.g. 'emote laughs out loud' -> 'Mark laughs | ||
9 | out loud' | ||
10 | * A 'whisper' command for talking to individual players | ||
11 | * A 'shout' command for yelling to players in all rooms | ||
12 | * Items to look at in rooms e.g. 'look fireplace' -> 'You see a | ||
13 | roaring, glowing fire' | ||
14 | * Items to pick up e.g. 'take rock' -> 'You pick up the rock' | ||
15 | * Monsters to fight | ||
16 | * Loot to collect | ||
17 | * Saving players accounts between sessions | ||
18 | * A password login | ||
19 | * A shop from which to buy items | ||
20 | |||
21 | author: Mark Frimston - mfrimston@gmail.com | ||
22 | """ | ||
23 | |||
24 | import time | ||
25 | # import datetime | ||
26 | #import io | ||
27 | import json | ||
28 | |||
29 | # import the MUD server class | ||
30 | from mudserver import MudServer | ||
31 | from commands import CommandHandler | ||
32 | |||
33 | print('STARTING MUD\r\n\r\n\r\n') | ||
34 | |||
35 | rooms = {} | ||
36 | |||
37 | # stores the players in the game | ||
38 | players = {} | ||
39 | |||
40 | # start the server | ||
41 | mud = MudServer() | ||
42 | |||
43 | cmd_handler = CommandHandler() | ||
44 | |||
45 | def load_room(room_name): | ||
46 | print("Loading room:" + room_name) | ||
47 | try: | ||
48 | if room_name in rooms: | ||
49 | return rooms[room_name] | ||
50 | |||
51 | |||
52 | module = __import__('rooms.' + room_name.lower()) | ||
53 | room_module = getattr(module, room_name.lower()) | ||
54 | room_class = getattr(room_module, room_name) | ||
55 | instance = room_class() | ||
56 | rooms[room_name] = instance | ||
57 | |||
58 | return instance | ||
59 | except Exception as e: | ||
60 | print(e) | ||
61 | return None | ||
62 | |||
63 | def save_object_to_file(obj, filename): | ||
64 | with open(filename, 'w', encoding='utf-8') as f: | ||
65 | f.write(json.dumps(obj, ensure_ascii=False)) | ||
66 | |||
67 | def load_object_from_file(filename): | ||
68 | try: | ||
69 | with open(filename, 'r', encoding='utf-8') as f: | ||
70 | return json.loads(f.read()) | ||
71 | except Exception: | ||
72 | return None | ||
73 | |||
74 | def prompt(pid): | ||
75 | if "prompt" not in players[pid]: | ||
76 | players[pid]["prompt"] = "> " | ||
77 | mud.send_message(pid, "\r\n" + players[pid]["prompt"], '') | ||
78 | |||
79 | # main game loop. We loop forever (i.e. until the program is terminated) | ||
80 | while True: | ||
81 | |||
82 | # pause for 1/5 of a second on each loop, so that we don't constantly | ||
83 | # use 100% CPU time | ||
84 | time.sleep(0.001) | ||
85 | |||
86 | # TODO: Add some cache removal if older than X | ||
87 | # now = datetime.datetime.now() | ||
88 | # delta = now - datetime.timedelta(minutes = 2) | ||
89 | # for room in rooms: | ||
90 | # if room.created < delta: | ||
91 | # del rooms[room] | ||
92 | |||
93 | # 'update' must be called in the loop to keep the game running and give | ||
94 | # us up-to-date information | ||
95 | mud.update() | ||
96 | |||
97 | # go through any newly connected players | ||
98 | for id in mud.get_new_players(): | ||
99 | |||
100 | # add the new player to the dictionary, noting that they've not been | ||
101 | # named yet. | ||
102 | # The dictionary key is the player's id number. We set their room to | ||
103 | # None initially until they have entered a name | ||
104 | # Try adding more player stats - level, gold, inventory, etc | ||
105 | players[id] = { | ||
106 | "name": None, | ||
107 | "room": None, | ||
108 | "inventory": None, | ||
109 | "prompt": "> ", | ||
110 | } | ||
111 | |||
112 | # send the new player a prompt for their name | ||
113 | mud.send_message(id, "What is your name?") | ||
114 | |||
115 | # go through any recently disconnected players | ||
116 | for id in mud.get_disconnected_players(): | ||
117 | |||
118 | # if for any reason the player isn't in the player map, skip them and | ||
119 | # move on to the next one | ||
120 | if id not in players: | ||
121 | continue | ||
122 | |||
123 | # go through all the players in the game | ||
124 | for pid, pl in players.items(): | ||
125 | # send each player a message to tell them about the diconnected | ||
126 | # player | ||
127 | mud.send_message(pid, "{} quit the game".format( | ||
128 | players[id]["name"])) | ||
129 | |||
130 | # remove the player's entry in the player dictionary | ||
131 | del(players[id]) | ||
132 | |||
133 | # go through any new commands sent from players | ||
134 | for id, command, params in mud.get_commands(): | ||
135 | |||
136 | # if for any reason the player isn't in the player map, skip them and | ||
137 | # move on to the next one | ||
138 | if id not in players: | ||
139 | continue | ||
140 | |||
141 | if players[id]["room"]: | ||
142 | rm = load_room(players[id]["room"]) | ||
143 | # if the player hasn't given their name yet, use this first command as | ||
144 | # their name and move them to the starting room. | ||
145 | if players[id]["name"] is None: | ||
146 | |||
147 | loaded_player = load_object_from_file("players/{}.json".format(command)) | ||
148 | if loaded_player is None: | ||
149 | players[id]["name"] = command | ||
150 | players[id]["room"] = "Tavern" | ||
151 | else: | ||
152 | players[id] = loaded_player | ||
153 | |||
154 | # go through all the players in the game | ||
155 | for pid, pl in players.items(): | ||
156 | # send each player a message to tell them about the new player | ||
157 | mud.send_message(pid, "{} entered the game".format( | ||
158 | players[id]["name"])) | ||
159 | |||
160 | # send the new player a welcome message | ||
161 | mud.send_message(id, "\r\n\r\nWelcome to the game, {}. ".format( | ||
162 | players[id]["name"]) | ||
163 | + "\r\nType 'help' for a list of commands. Have fun!\r\n\r\n") | ||
164 | |||
165 | # send the new player the description of their current room | ||
166 | mud.send_message(id, load_room(players[id]["room"]).get_description()) | ||
167 | |||
168 | # each of the possible commands is handled below. Try adding new | ||
169 | # commands to the game! | ||
170 | |||
171 | # 'help' command | ||
172 | elif command == "help": | ||
173 | |||
174 | with open('help/help.txt', 'r', encoding='utf-8') as f: | ||
175 | for line in f: | ||
176 | mud.send_message(id, line, '\r') | ||
177 | mud.send_message(id, '') | ||
178 | |||
179 | # 'help' command | ||
180 | elif command == "quit": | ||
181 | |||
182 | # send the player back the list of possible commands | ||
183 | mud.send_message(id, "Saving...") | ||
184 | save_object_to_file(players[id], "players/{}.json".format(players[id]["name"])) | ||
185 | mud.disconnect_player(id) | ||
186 | |||
187 | # 'help' command | ||
188 | elif command == "save": | ||
189 | |||
190 | # send the player back the list of possible commands | ||
191 | mud.send_message(id, "Saving...") | ||
192 | save_object_to_file(players[id], "players/{}.json".format(players[id]["name"])) | ||
193 | mud.send_message(id, "Save complete") | ||
194 | |||
195 | # 'say' command | ||
196 | elif command == "say": | ||
197 | |||
198 | # go through every player in the game | ||
199 | for pid, pl in players.items(): | ||
200 | # if they're in the same room as the player | ||
201 | if players[pid]["room"] == players[id]["room"]: | ||
202 | # send them a message telling them what the player said | ||
203 | mud.send_message(pid, "{} says: {}".format( | ||
204 | players[id]["name"], params)) | ||
205 | |||
206 | # 'look' command | ||
207 | elif cmd_handler.parse(id, command, params, rm, mud, players): | ||
208 | |||
209 | pass | ||
210 | |||
211 | # 'go' command | ||
212 | elif command == "go": | ||
213 | |||
214 | # store the exit name | ||
215 | ex = params.lower() | ||
216 | |||
217 | # store the player's current room | ||
218 | rm = load_room(players[id]["room"]) | ||
219 | |||
220 | # if the specified exit is found in the room's exits list | ||
221 | if ex in rm.get_exits(): | ||
222 | |||
223 | # go through all the players in the game | ||
224 | for pid, pl in players.items(): | ||
225 | # if player is in the same room and isn't the player | ||
226 | # sending the command | ||
227 | if players[pid]["room"] == players[id]["room"] \ | ||
228 | and pid != id: | ||
229 | # send them a message telling them that the player | ||
230 | # left the room | ||
231 | mud.send_message(pid, "{} left via exit '{}'".format( | ||
232 | players[id]["name"], ex)) | ||
233 | |||
234 | # update the player's current room to the one the exit leads to | ||
235 | loaded = load_room(rm.get_exits()[ex]) | ||
236 | if loaded == None: | ||
237 | mud.send_message(id, "An invisible force prevents you from going in that direction.") | ||
238 | continue | ||
239 | else: | ||
240 | players[id]["room"] = rm.get_exits()[ex] | ||
241 | rm = loaded | ||
242 | |||
243 | # go through all the players in the game | ||
244 | for pid, pl in players.items(): | ||
245 | # if player is in the same (new) room and isn't the player | ||
246 | # sending the command | ||
247 | if players[pid]["room"] == players[id]["room"] \ | ||
248 | and pid != id: | ||
249 | # send them a message telling them that the player | ||
250 | # entered the room | ||
251 | mud.send_message(pid, | ||
252 | "{} arrived via exit '{}'".format( | ||
253 | players[id]["name"], ex)) | ||
254 | |||
255 | # send the player a message telling them where they are now | ||
256 | mud.send_message(id, "You arrive at '{}'".format(rm.get_title())) | ||
257 | |||
258 | # the specified exit wasn't found in the current room | ||
259 | else: | ||
260 | # send back an 'unknown exit' message | ||
261 | mud.send_message(id, "Unknown exit '{}'".format(ex)) | ||
262 | |||
263 | |||
264 | # some other, unrecognised command | ||
265 | else: | ||
266 | # send back an 'unknown command' message | ||
267 | mud.send_message(id, "Unknown command '{}'".format(command)) | ||
268 | prompt(id) |
mudserver.py
0 → 100644
1 | """Basic MUD server module for creating text-based Multi-User Dungeon | ||
2 | (MUD) games. | ||
3 | |||
4 | Contains one class, MudServer, which can be instantiated to start a | ||
5 | server running then used to send and receive messages from players. | ||
6 | |||
7 | author: Mark Frimston - mfrimston@gmail.com | ||
8 | """ | ||
9 | |||
10 | |||
11 | import socket | ||
12 | import select | ||
13 | import time | ||
14 | import sys | ||
15 | |||
16 | |||
17 | class MudServer(object): | ||
18 | """A basic server for text-based Multi-User Dungeon (MUD) games. | ||
19 | |||
20 | Once created, the server will listen for players connecting using | ||
21 | Telnet. Messages can then be sent to and from multiple connected | ||
22 | players. | ||
23 | |||
24 | The 'update' method should be called in a loop to keep the server | ||
25 | running. | ||
26 | """ | ||
27 | |||
28 | # An inner class which is instantiated for each connected client to store | ||
29 | # info about them | ||
30 | |||
31 | class _Client(object): | ||
32 | """Holds information about a connected player""" | ||
33 | |||
34 | # the socket object used to communicate with this client | ||
35 | socket = None | ||
36 | # the ip address of this client | ||
37 | address = "" | ||
38 | # holds data send from the client until a full message is received | ||
39 | buffer = "" | ||
40 | # the last time we checked if the client was still connected | ||
41 | lastcheck = 0 | ||
42 | |||
43 | def __init__(self, socket, address, buffer, lastcheck): | ||
44 | self.socket = socket | ||
45 | self.address = address | ||
46 | self.buffer = buffer | ||
47 | self.lastcheck = lastcheck | ||
48 | |||
49 | # Used to store different types of occurences | ||
50 | _EVENT_NEW_PLAYER = 1 | ||
51 | _EVENT_PLAYER_LEFT = 2 | ||
52 | _EVENT_COMMAND = 3 | ||
53 | |||
54 | # Different states we can be in while reading data from client | ||
55 | # See _process_sent_data function | ||
56 | _READ_STATE_NORMAL = 1 | ||
57 | _READ_STATE_COMMAND = 2 | ||
58 | _READ_STATE_SUBNEG = 3 | ||
59 | |||
60 | # Command codes used by Telnet protocol | ||
61 | # See _process_sent_data function | ||
62 | _TN_INTERPRET_AS_COMMAND = 255 | ||
63 | _TN_ARE_YOU_THERE = 246 | ||
64 | _TN_WILL = 251 | ||
65 | _TN_WONT = 252 | ||
66 | _TN_DO = 253 | ||
67 | _TN_DONT = 254 | ||
68 | _TN_SUBNEGOTIATION_START = 250 | ||
69 | _TN_SUBNEGOTIATION_END = 240 | ||
70 | |||
71 | # socket used to listen for new clients | ||
72 | _listen_socket = None | ||
73 | # holds info on clients. Maps client id to _Client object | ||
74 | _clients = {} | ||
75 | # counter for assigning each client a new id | ||
76 | _nextid = 0 | ||
77 | # list of occurences waiting to be handled by the code | ||
78 | _events = [] | ||
79 | # list of newly-added occurences | ||
80 | _new_events = [] | ||
81 | |||
82 | def __init__(self): | ||
83 | """Constructs the MudServer object and starts listening for | ||
84 | new players. | ||
85 | """ | ||
86 | |||
87 | self._clients = {} | ||
88 | self._nextid = 0 | ||
89 | self._events = [] | ||
90 | self._new_events = [] | ||
91 | |||
92 | # create a new tcp socket which will be used to listen for new clients | ||
93 | self._listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
94 | |||
95 | # set a special option on the socket which allows the port to be | ||
96 | # immediately without having to wait | ||
97 | self._listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, | ||
98 | 1) | ||
99 | |||
100 | # bind the socket to an ip address and port. Port 23 is the standard | ||
101 | # telnet port which telnet clients will use, however on some platforms | ||
102 | # this requires root permissions, so we use a higher arbitrary port | ||
103 | # number instead: 1234. Address 0.0.0.0 means that we will bind to all | ||
104 | # of the available network interfaces | ||
105 | self._listen_socket.bind(("0.0.0.0", 1234)) | ||
106 | |||
107 | # set to non-blocking mode. This means that when we call 'accept', it | ||
108 | # will return immediately without waiting for a connection | ||
109 | self._listen_socket.setblocking(False) | ||
110 | |||
111 | # start listening for connections on the socket | ||
112 | self._listen_socket.listen(1) | ||
113 | |||
114 | def update(self): | ||
115 | """Checks for new players, disconnected players, and new | ||
116 | messages sent from players. This method must be called before | ||
117 | up-to-date info can be obtained from the 'get_new_players', | ||
118 | 'get_disconnected_players' and 'get_commands' methods. | ||
119 | It should be called in a loop to keep the game running. | ||
120 | """ | ||
121 | |||
122 | # check for new stuff | ||
123 | self._check_for_new_connections() | ||
124 | self._check_for_disconnected() | ||
125 | self._check_for_messages() | ||
126 | |||
127 | # move the new events into the main events list so that they can be | ||
128 | # obtained with 'get_new_players', 'get_disconnected_players' and | ||
129 | # 'get_commands'. The previous events are discarded | ||
130 | self._events = list(self._new_events) | ||
131 | self._new_events = [] | ||
132 | |||
133 | def disconnect_player(self, clid): | ||
134 | cl = self._clients[clid] | ||
135 | cl.socket.close() | ||
136 | self._handle_disconnect(clid) | ||
137 | |||
138 | def get_new_players(self): | ||
139 | """Returns a list containing info on any new players that have | ||
140 | entered the game since the last call to 'update'. Each item in | ||
141 | the list is a player id number. | ||
142 | """ | ||
143 | retval = [] | ||
144 | # go through all the events in the main list | ||
145 | for ev in self._events: | ||
146 | # if the event is a new player occurence, add the info to the list | ||
147 | if ev[0] == self._EVENT_NEW_PLAYER: | ||
148 | retval.append(ev[1]) | ||
149 | # return the info list | ||
150 | return retval | ||
151 | |||
152 | def get_disconnected_players(self): | ||
153 | """Returns a list containing info on any players that have left | ||
154 | the game since the last call to 'update'. Each item in the list | ||
155 | is a player id number. | ||
156 | """ | ||
157 | retval = [] | ||
158 | # go through all the events in the main list | ||
159 | for ev in self._events: | ||
160 | # if the event is a player disconnect occurence, add the info to | ||
161 | # the list | ||
162 | if ev[0] == self._EVENT_PLAYER_LEFT: | ||
163 | retval.append(ev[1]) | ||
164 | # return the info list | ||
165 | return retval | ||
166 | |||
167 | def get_commands(self): | ||
168 | """Returns a list containing any commands sent from players | ||
169 | since the last call to 'update'. Each item in the list is a | ||
170 | 3-tuple containing the id number of the sending player, a | ||
171 | string containing the command (i.e. the first word of what | ||
172 | they typed), and another string containing the text after the | ||
173 | command | ||
174 | """ | ||
175 | retval = [] | ||
176 | # go through all the events in the main list | ||
177 | for ev in self._events: | ||
178 | # if the event is a command occurence, add the info to the list | ||
179 | if ev[0] == self._EVENT_COMMAND: | ||
180 | retval.append((ev[1], ev[2], ev[3])) | ||
181 | # return the info list | ||
182 | return retval | ||
183 | |||
184 | def send_message(self, to, message, line_ending='\r\n'): | ||
185 | """Sends the text in the 'message' parameter to the player with | ||
186 | the id number given in the 'to' parameter. The text will be | ||
187 | printed out in the player's terminal. | ||
188 | """ | ||
189 | # we make sure to put a newline on the end so the client receives the | ||
190 | # message on its own line | ||
191 | self._attempt_send(to, message + line_ending) | ||
192 | |||
193 | def shutdown(self): | ||
194 | """Closes down the server, disconnecting all clients and | ||
195 | closing the listen socket. | ||
196 | """ | ||
197 | # for each client | ||
198 | for cl in self._clients.values(): | ||
199 | # close the socket, disconnecting the client | ||
200 | cl.socket.shutdown() | ||
201 | cl.socket.close() | ||
202 | # stop listening for new clients | ||
203 | self._listen_socket.close() | ||
204 | |||
205 | def _attempt_send(self, clid, data): | ||
206 | # python 2/3 compatability fix - convert non-unicode string to unicode | ||
207 | if sys.version < '3' and type(data) != unicode: | ||
208 | data = unicode(data, "latin1") | ||
209 | try: | ||
210 | # look up the client in the client map and use 'sendall' to send | ||
211 | # the message string on the socket. 'sendall' ensures that all of | ||
212 | # the data is sent in one go | ||
213 | if sys.version_info != (3, 4, 0): | ||
214 | bytes_to_send = bytearray(data, 'latin1') | ||
215 | else: | ||
216 | bytes_to_send = bytearray(data) | ||
217 | |||
218 | client_socket = self._clients[clid].socket | ||
219 | client_socket.sendall(bytes_to_send) | ||
220 | # KeyError will be raised if there is no client with the given id in | ||
221 | # the map | ||
222 | except KeyError: | ||
223 | pass | ||
224 | # If there is a connection problem with the client (e.g. they have | ||
225 | # disconnected) a socket error will be raised | ||
226 | except Exception as e: | ||
227 | sys.print_exception(e) | ||
228 | self._handle_disconnect(clid) | ||
229 | |||
230 | def _check_for_new_connections(self): | ||
231 | |||
232 | # 'select' is used to check whether there is data waiting to be read | ||
233 | # from the socket. We pass in 3 lists of sockets, the first being those | ||
234 | # to check for readability. It returns 3 lists, the first being | ||
235 | # the sockets that are readable. The last parameter is how long to wait | ||
236 | # - we pass in 0 so that it returns immediately without waiting | ||
237 | rlist, wlist, xlist = select.select([self._listen_socket], [], [], 0) | ||
238 | |||
239 | # if the socket wasn't in the readable list, there's no data available, | ||
240 | # meaning no clients waiting to connect, and so we can exit the method | ||
241 | # here | ||
242 | if self._listen_socket not in rlist: | ||
243 | return | ||
244 | |||
245 | # 'accept' returns a new socket and address info which can be used to | ||
246 | # communicate with the new client | ||
247 | joined_socket, addr = self._listen_socket.accept() | ||
248 | |||
249 | # set non-blocking mode on the new socket. This means that 'send' and | ||
250 | # 'recv' will return immediately without waiting | ||
251 | joined_socket.setblocking(False) | ||
252 | |||
253 | # construct a new _Client object to hold info about the newly connected | ||
254 | # client. Use 'nextid' as the new client's id number | ||
255 | self._clients[self._nextid] = MudServer._Client(joined_socket, addr[0], | ||
256 | "", time.time()) | ||
257 | |||
258 | # add a new player occurence to the new events list with the player's | ||
259 | # id number | ||
260 | self._new_events.append((self._EVENT_NEW_PLAYER, self._nextid)) | ||
261 | |||
262 | # add 1 to 'nextid' so that the next client to connect will get a | ||
263 | # unique id number | ||
264 | self._nextid += 1 | ||
265 | |||
266 | def _check_for_disconnected(self): | ||
267 | |||
268 | # go through all the clients | ||
269 | for id, cl in list(self._clients.items()): | ||
270 | |||
271 | # if we last checked the client less than 5 seconds ago, skip this | ||
272 | # client and move on to the next one | ||
273 | if time.time() - cl.lastcheck < 5.0: | ||
274 | continue | ||
275 | |||
276 | # send the client an invisible character. It doesn't actually | ||
277 | # matter what we send, we're really just checking that data can | ||
278 | # still be written to the socket. If it can't, an error will be | ||
279 | # raised and we'll know that the client has disconnected. | ||
280 | self._attempt_send(id, "\x00") | ||
281 | |||
282 | # update the last check time | ||
283 | cl.lastcheck = time.time() | ||
284 | |||
285 | def _check_for_messages(self): | ||
286 | |||
287 | # go through all the clients | ||
288 | for id, cl in list(self._clients.items()): | ||
289 | |||
290 | # we use 'select' to test whether there is data waiting to be read | ||
291 | # from the client socket. The function takes 3 lists of sockets, | ||
292 | # the first being those to test for readability. It returns 3 list | ||
293 | # of sockets, the first being those that are actually readable. | ||
294 | rlist, wlist, xlist = select.select([cl.socket], [], [], 0) | ||
295 | |||
296 | # if the client socket wasn't in the readable list, there is no | ||
297 | # new data from the client - we can skip it and move on to the next | ||
298 | # one | ||
299 | if cl.socket not in rlist: | ||
300 | continue | ||
301 | |||
302 | try: | ||
303 | # read data from the socket, using a max length of 4096 | ||
304 | data = cl.socket.recv(4096).decode("latin1") | ||
305 | |||
306 | # process the data, stripping out any special Telnet commands | ||
307 | message = self._process_sent_data(cl, data) | ||
308 | |||
309 | # if there was a message in the data | ||
310 | if message: | ||
311 | |||
312 | # remove any spaces, tabs etc from the start and end of | ||
313 | # the message | ||
314 | message = message.strip() | ||
315 | |||
316 | # separate the message into the command (the first word) | ||
317 | # and its parameters (the rest of the message) | ||
318 | command, params = (message.split(" ", 1) + ["", ""])[:2] | ||
319 | |||
320 | # add a command occurence to the new events list with the | ||
321 | # player's id number, the command and its parameters | ||
322 | self._new_events.append((self._EVENT_COMMAND, id, | ||
323 | command.lower(), params)) | ||
324 | |||
325 | # if there is a problem reading from the socket (e.g. the client | ||
326 | # has disconnected) a socket error will be raised | ||
327 | except socket.error: | ||
328 | self._handle_disconnect(id) | ||
329 | |||
330 | def _handle_disconnect(self, clid): | ||
331 | |||
332 | # remove the client from the clients map | ||
333 | del(self._clients[clid]) | ||
334 | |||
335 | # add a 'player left' occurence to the new events list, with the | ||
336 | # player's id number | ||
337 | self._new_events.append((self._EVENT_PLAYER_LEFT, clid)) | ||
338 | |||
339 | def _process_sent_data(self, client, data): | ||
340 | |||
341 | # the Telnet protocol allows special command codes to be inserted into | ||
342 | # messages. For our very simple server we don't need to response to | ||
343 | # any of these codes, but we must at least detect and skip over them | ||
344 | # so that we don't interpret them as text data. | ||
345 | # More info on the Telnet protocol can be found here: | ||
346 | # http://pcmicro.com/netfoss/telnet.html | ||
347 | |||
348 | # start with no message and in the normal state | ||
349 | message = None | ||
350 | state = self._READ_STATE_NORMAL | ||
351 | |||
352 | # go through the data a character at a time | ||
353 | for c in data: | ||
354 | |||
355 | # handle the character differently depending on the state we're in: | ||
356 | |||
357 | # normal state | ||
358 | if state == self._READ_STATE_NORMAL: | ||
359 | |||
360 | # if we received the special 'interpret as command' code, | ||
361 | # switch to 'command' state so that we handle the next | ||
362 | # character as a command code and not as regular text data | ||
363 | if ord(c) == self._TN_INTERPRET_AS_COMMAND: | ||
364 | state = self._READ_STATE_COMMAND | ||
365 | |||
366 | # if we get a newline character, this is the end of the | ||
367 | # message. Set 'message' to the contents of the buffer and | ||
368 | # clear the buffer | ||
369 | elif c == "\n": | ||
370 | message = client.buffer | ||
371 | client.buffer = "" | ||
372 | |||
373 | # some telnet clients send the characters as soon as the user | ||
374 | # types them. So if we get a backspace character, this is where | ||
375 | # the user has deleted a character and we should delete the | ||
376 | # last character from the buffer. | ||
377 | elif c == "\x08": | ||
378 | client.buffer = client.buffer[:-1] | ||
379 | |||
380 | # otherwise it's just a regular character - add it to the | ||
381 | # buffer where we're building up the received message | ||
382 | else: | ||
383 | client.buffer += c | ||
384 | |||
385 | # command state | ||
386 | elif state == self._READ_STATE_COMMAND: | ||
387 | |||
388 | # the special 'start of subnegotiation' command code indicates | ||
389 | # that the following characters are a list of options until | ||
390 | # we're told otherwise. We switch into 'subnegotiation' state | ||
391 | # to handle this | ||
392 | if ord(c) == self._TN_SUBNEGOTIATION_START: | ||
393 | state = self._READ_STATE_SUBNEG | ||
394 | |||
395 | # if the command code is one of the 'will', 'wont', 'do' or | ||
396 | # 'dont' commands, the following character will be an option | ||
397 | # code so we must remain in the 'command' state | ||
398 | elif ord(c) in (self._TN_WILL, self._TN_WONT, self._TN_DO, | ||
399 | self._TN_DONT): | ||
400 | state = self._READ_STATE_COMMAND | ||
401 | |||
402 | # for all other command codes, there is no accompanying data so | ||
403 | # we can return to 'normal' state. | ||
404 | else: | ||
405 | state = self._READ_STATE_NORMAL | ||
406 | |||
407 | # subnegotiation state | ||
408 | elif state == self._READ_STATE_SUBNEG: | ||
409 | |||
410 | # if we reach an 'end of subnegotiation' command, this ends the | ||
411 | # list of options and we can return to 'normal' state. | ||
412 | # Otherwise we must remain in this state | ||
413 | if ord(c) == self._TN_SUBNEGOTIATION_END: | ||
414 | state = self._READ_STATE_NORMAL | ||
415 | |||
416 | # return the contents of 'message' which is either a string or None | ||
417 | return message |
players/test.json
0 → 100644
1 | {"name": "test", "room": "Room001", "inventory": null, "prompt": "> "} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
rooms/__init__.py
0 → 100644
File mode changed
rooms/baseroom.py
0 → 100644
1 | |||
2 | # import datetime | ||
3 | |||
4 | class BaseRoom(object): | ||
5 | |||
6 | def __init__(self, title, description, exits, look_items): | ||
7 | self.title = title | ||
8 | self.description = description | ||
9 | self.exits = exits | ||
10 | self.look_items = look_items | ||
11 | # self.created = datetime.datetime.now() | ||
12 | |||
13 | def get_title(self): | ||
14 | return self.title | ||
15 | |||
16 | def get_description(self): | ||
17 | return self.description | ||
18 | |||
19 | def get_exits(self): | ||
20 | return self.exits | ||
21 | |||
22 | def get_look_items(self): | ||
23 | return self.look_items |
rooms/room001.py
0 → 100644
1 | from .baseroom import BaseRoom | ||
2 | |||
3 | class Room001(BaseRoom): | ||
4 | |||
5 | def __init__(self): | ||
6 | |||
7 | title = "Behind the bar" | ||
8 | description = "The back of the bar gives a full view of the tavern. The bar\r\n top is a large wooden plank thrown across some barrels." | ||
9 | exits = {"tavern": "Tavern"} | ||
10 | look_items = { | ||
11 | "bar": "The bar is a long wooden plank thrown over roughly hewn barrels.", | ||
12 | "barrel,barrels": "The old barrels bands are thick with oxidation and stained\r\n with the purple of spilled wine.", | ||
13 | "wooden,oak,plank": "An old solid oak plank that has a large number of chips\r\n and drink marks." | ||
14 | } | ||
15 | |||
16 | super(Room001, self).__init__(title, description, exits, look_items) |
rooms/tavern.py
0 → 100644
1 | from .baseroom import BaseRoom | ||
2 | |||
3 | class Tavern(BaseRoom): | ||
4 | |||
5 | def __init__(self): | ||
6 | |||
7 | title = "Tavern" | ||
8 | description = "You're in a cozy tavern warmed by an open fire." | ||
9 | exits = {"outside": "Outside", "behind bar": "Room001"} | ||
10 | look_items = { | ||
11 | "bar": "The bar is a long wooden plank thrown over roughly hewn barrels.", | ||
12 | "barrel,barrels": "The old barrels bands are thick with oxidation and stained\r\n with the purple of spilled wine.", | ||
13 | "wooden,oak,plank": "An old solid oak plank that has a large number of chips\r\n and drink marks.", | ||
14 | "fire": "The fire crackles quietly in the corner providing a small amount of light and heat." | ||
15 | } | ||
16 | |||
17 | super(Tavern, self).__init__(title, description, exits, look_items) |
simplemud.py
0 → 100644
1 | #!/usr/bin/env python | ||
2 | |||
3 | """A simple Multi-User Dungeon (MUD) game. Players can talk to each | ||
4 | other, examine their surroundings and move between rooms. | ||
5 | |||
6 | Some ideas for things to try adding: | ||
7 | * More rooms to explore | ||
8 | * An 'emote' command e.g. 'emote laughs out loud' -> 'Mark laughs | ||
9 | out loud' | ||
10 | * A 'whisper' command for talking to individual players | ||
11 | * A 'shout' command for yelling to players in all rooms | ||
12 | * Items to look at in rooms e.g. 'look fireplace' -> 'You see a | ||
13 | roaring, glowing fire' | ||
14 | * Items to pick up e.g. 'take rock' -> 'You pick up the rock' | ||
15 | * Monsters to fight | ||
16 | * Loot to collect | ||
17 | * Saving players accounts between sessions | ||
18 | * A password login | ||
19 | * A shop from which to buy items | ||
20 | |||
21 | author: Mark Frimston - mfrimston@gmail.com | ||
22 | """ | ||
23 | |||
24 | import time | ||
25 | |||
26 | # import the MUD server class | ||
27 | from mudserver import MudServer | ||
28 | |||
29 | |||
30 | # structure defining the rooms in the game. Try adding more rooms to the game! | ||
31 | rooms = { | ||
32 | "Tavern": { | ||
33 | "description": "You're in a cozy tavern warmed by an open fire.", | ||
34 | "exits": {"outside": "Outside"}, | ||
35 | }, | ||
36 | "Outside": { | ||
37 | "description": "You're standing outside a tavern. It's raining.", | ||
38 | "exits": {"inside": "Tavern"}, | ||
39 | } | ||
40 | } | ||
41 | |||
42 | # stores the players in the game | ||
43 | players = {} | ||
44 | |||
45 | # start the server | ||
46 | mud = MudServer() | ||
47 | |||
48 | # main game loop. We loop forever (i.e. until the program is terminated) | ||
49 | while True: | ||
50 | |||
51 | # pause for 1/5 of a second on each loop, so that we don't constantly | ||
52 | # use 100% CPU time | ||
53 | time.sleep(0.2) | ||
54 | |||
55 | # 'update' must be called in the loop to keep the game running and give | ||
56 | # us up-to-date information | ||
57 | mud.update() | ||
58 | |||
59 | # go through any newly connected players | ||
60 | for id in mud.get_new_players(): | ||
61 | |||
62 | # add the new player to the dictionary, noting that they've not been | ||
63 | # named yet. | ||
64 | # The dictionary key is the player's id number. We set their room to | ||
65 | # None initially until they have entered a name | ||
66 | # Try adding more player stats - level, gold, inventory, etc | ||
67 | players[id] = { | ||
68 | "name": None, | ||
69 | "room": None, | ||
70 | } | ||
71 | |||
72 | # send the new player a prompt for their name | ||
73 | mud.send_message(id, "What is your name?") | ||
74 | |||
75 | # go through any recently disconnected players | ||
76 | for id in mud.get_disconnected_players(): | ||
77 | |||
78 | # if for any reason the player isn't in the player map, skip them and | ||
79 | # move on to the next one | ||
80 | if id not in players: | ||
81 | continue | ||
82 | |||
83 | # go through all the players in the game | ||
84 | for pid, pl in players.items(): | ||
85 | # send each player a message to tell them about the diconnected | ||
86 | # player | ||
87 | mud.send_message(pid, "{} quit the game".format( | ||
88 | players[id]["name"])) | ||
89 | |||
90 | # remove the player's entry in the player dictionary | ||
91 | del(players[id]) | ||
92 | |||
93 | # go through any new commands sent from players | ||
94 | for id, command, params in mud.get_commands(): | ||
95 | |||
96 | # if for any reason the player isn't in the player map, skip them and | ||
97 | # move on to the next one | ||
98 | if id not in players: | ||
99 | continue | ||
100 | |||
101 | # if the player hasn't given their name yet, use this first command as | ||
102 | # their name and move them to the starting room. | ||
103 | if players[id]["name"] is None: | ||
104 | |||
105 | players[id]["name"] = command | ||
106 | players[id]["room"] = "Tavern" | ||
107 | |||
108 | # go through all the players in the game | ||
109 | for pid, pl in players.items(): | ||
110 | # send each player a message to tell them about the new player | ||
111 | mud.send_message(pid, "{} entered the game".format( | ||
112 | players[id]["name"])) | ||
113 | |||
114 | # send the new player a welcome message | ||
115 | mud.send_message(id, "Welcome to the game, {}. ".format( | ||
116 | players[id]["name"]) | ||
117 | + "Type 'help' for a list of commands. Have fun!") | ||
118 | |||
119 | # send the new player the description of their current room | ||
120 | mud.send_message(id, rooms[players[id]["room"]]["description"]) | ||
121 | |||
122 | # each of the possible commands is handled below. Try adding new | ||
123 | # commands to the game! | ||
124 | |||
125 | # 'help' command | ||
126 | elif command == "help": | ||
127 | |||
128 | # send the player back the list of possible commands | ||
129 | mud.send_message(id, "Commands:") | ||
130 | mud.send_message(id, " say <message> - Says something out loud, " | ||
131 | + "e.g. 'say Hello'") | ||
132 | mud.send_message(id, " look - Examines the " | ||
133 | + "surroundings, e.g. 'look'") | ||
134 | mud.send_message(id, " go <exit> - Moves through the exit " | ||
135 | + "specified, e.g. 'go outside'") | ||
136 | |||
137 | # 'say' command | ||
138 | elif command == "say": | ||
139 | |||
140 | # go through every player in the game | ||
141 | for pid, pl in players.items(): | ||
142 | # if they're in the same room as the player | ||
143 | if players[pid]["room"] == players[id]["room"]: | ||
144 | # send them a message telling them what the player said | ||
145 | mud.send_message(pid, "{} says: {}".format( | ||
146 | players[id]["name"], params)) | ||
147 | |||
148 | # 'look' command | ||
149 | elif command == "look": | ||
150 | |||
151 | # store the player's current room | ||
152 | rm = rooms[players[id]["room"]] | ||
153 | |||
154 | # send the player back the description of their current room | ||
155 | mud.send_message(id, rm["description"]) | ||
156 | |||
157 | playershere = [] | ||
158 | # go through every player in the game | ||
159 | for pid, pl in players.items(): | ||
160 | # if they're in the same room as the player | ||
161 | if players[pid]["room"] == players[id]["room"]: | ||
162 | # ... and they have a name to be shown | ||
163 | if players[pid]["name"] is not None: | ||
164 | # add their name to the list | ||
165 | playershere.append(players[pid]["name"]) | ||
166 | |||
167 | # send player a message containing the list of players in the room | ||
168 | mud.send_message(id, "Players here: {}".format( | ||
169 | ", ".join(playershere))) | ||
170 | |||
171 | # send player a message containing the list of exits from this room | ||
172 | mud.send_message(id, "Exits are: {}".format( | ||
173 | ", ".join(rm["exits"]))) | ||
174 | |||
175 | # 'go' command | ||
176 | elif command == "go": | ||
177 | |||
178 | # store the exit name | ||
179 | ex = params.lower() | ||
180 | |||
181 | # store the player's current room | ||
182 | rm = rooms[players[id]["room"]] | ||
183 | |||
184 | # if the specified exit is found in the room's exits list | ||
185 | if ex in rm["exits"]: | ||
186 | |||
187 | # go through all the players in the game | ||
188 | for pid, pl in players.items(): | ||
189 | # if player is in the same room and isn't the player | ||
190 | # sending the command | ||
191 | if players[pid]["room"] == players[id]["room"] \ | ||
192 | and pid != id: | ||
193 | # send them a message telling them that the player | ||
194 | # left the room | ||
195 | mud.send_message(pid, "{} left via exit '{}'".format( | ||
196 | players[id]["name"], ex)) | ||
197 | |||
198 | # update the player's current room to the one the exit leads to | ||
199 | players[id]["room"] = rm["exits"][ex] | ||
200 | rm = rooms[players[id]["room"]] | ||
201 | |||
202 | # go through all the players in the game | ||
203 | for pid, pl in players.items(): | ||
204 | # if player is in the same (new) room and isn't the player | ||
205 | # sending the command | ||
206 | if players[pid]["room"] == players[id]["room"] \ | ||
207 | and pid != id: | ||
208 | # send them a message telling them that the player | ||
209 | # entered the room | ||
210 | mud.send_message(pid, | ||
211 | "{} arrived via exit '{}'".format( | ||
212 | players[id]["name"], ex)) | ||
213 | |||
214 | # send the player a message telling them where they are now | ||
215 | mud.send_message(id, "You arrive at '{}'".format( | ||
216 | players[id]["room"])) | ||
217 | |||
218 | # the specified exit wasn't found in the current room | ||
219 | else: | ||
220 | # send back an 'unknown exit' message | ||
221 | mud.send_message(id, "Unknown exit '{}'".format(ex)) | ||
222 | |||
223 | # some other, unrecognised command | ||
224 | else: | ||
225 | # send back an 'unknown command' message | ||
226 | mud.send_message(id, "Unknown command '{}'".format(command)) |
-
Please register or sign in to post a comment