home · contact · privacy
Server/py: Optimize polling in read_command().
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7 import ctypes
8
9
10 class RandomnessIO:
11     """"Interface to libplomrogue's pseudo-randomness generator."""
12
13     def set_seed(self, seed):
14         libpr.seed_rrand(1, seed)
15
16     def get_seed(self):
17         return libpr.seed_rrand(0, 0)
18
19     def next(self):
20         return libpr.rrand()
21
22     seed = property(get_seed, set_seed)
23
24
25 def prep_library():
26     """Prepare ctypes library at ./libplomrogue.so"""
27     libpath = ("./libplomrogue.so")
28     if not os.access(libpath, os.F_OK):
29         raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
30     libpr = ctypes.cdll.LoadLibrary(libpath)
31     libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
32     libpr.seed_rrand.restype = ctypes.c_uint32
33     libpr.rrand.argtypes = []
34     libpr.rrand.restype = ctypes.c_uint16
35     libpr.set_maplength.argtypes = [ctypes.c_uint16]
36     libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8,
37                                               ctypes.c_uint8]
38     libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8
39     libpr.result_y.restype = ctypes.c_uint8
40     libpr.result_x.restype = ctypes.c_uint8
41     libpr.set_maplength(world_db["MAP_LENGTH"])
42     return libpr
43
44
45 def strong_write(file, string):
46     """Apply write(string), flush(), and os.fsync() to file."""
47     file.write(string)
48     file.flush()
49     os.fsync(file)
50
51
52 def setup_server_io():
53     """Fill IO files DB with proper file( path)s. Write process IO test string.
54
55     Ensure IO files directory at server/. Remove any old input file if found.
56     Set up new input file for reading, and new output file for writing. Start
57     output file with process hash line of format PID + " " + floated UNIX time
58     (io_db["teststring"]). Raise SystemExit if file is found at path of either
59     record or save file plus io_db["tmp_suffix"].
60     """
61     def detect_atomic_leftover(path, tmp_suffix):
62         path_tmp = path + tmp_suffix
63         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
64               "aborted previous attempt to write '" + path + "'. Aborting " \
65              "until matter is resolved by removing it from its current path."
66         if os.access(path_tmp, os.F_OK):
67             raise SystemExit(msg)
68     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
69     os.makedirs(io_db["path_server"], exist_ok=True)
70     io_db["file_out"] = open(io_db["path_out"], "w")
71     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
72     if os.access(io_db["path_in"], os.F_OK):
73         os.remove(io_db["path_in"])
74     io_db["file_in"] = open(io_db["path_in"], "w")
75     io_db["file_in"].close()
76     io_db["file_in"] = open(io_db["path_in"], "r")
77     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
78     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
79
80
81 def cleanup_server_io():
82     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
83     def helper(file_key, path_key):
84         if file_key in io_db:
85             io_db[file_key].close()
86             if not io_db["kicked_by_rival"] \
87                and os.access(io_db[path_key], os.F_OK):
88                 os.remove(io_db[path_key])
89     helper("file_out", "path_out")
90     helper("file_in", "path_in")
91     helper("file_worldstate", "path_worldstate")
92     if "file_record" in io_db:
93         io_db["file_record"].close()
94
95
96 def obey(command, prefix, replay=False, do_record=False):
97     """Call function from commands_db mapped to command's first token.
98
99     Tokenize command string with shlex.split(comments=True). If replay is set,
100     a non-meta command from the commands_db merely triggers obey() on the next
101     command from the records file. If not, non-meta commands set
102     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
103     do_record is set, are recorded via record(), and save_world() is called.
104     The prefix string is inserted into the server's input message between its
105     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
106     """
107     server_test()
108     print("input " + prefix + ": " + command)
109     try:
110         tokens = shlex.split(command, comments=True)
111     except ValueError as err:
112         print("Can't tokenize command string: " + str(err) + ".")
113         return
114     if len(tokens) > 0 and tokens[0] in commands_db \
115        and len(tokens) == commands_db[tokens[0]][0] + 1:
116         if commands_db[tokens[0]][1]:
117             commands_db[tokens[0]][2](*tokens[1:])
118         elif replay:
119             print("Due to replay mode, reading command as 'go on in record'.")
120             line = io_db["file_record"].readline()
121             if len(line) > 0:
122                 obey(line.rstrip(), io_db["file_record"].prefix
123                      + str(io_db["file_record"].line_n))
124                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
125             else:
126                 print("Reached end of record file.")
127         else:
128             commands_db[tokens[0]][2](*tokens[1:])
129             if do_record:
130                 record(command)
131                 save_world()
132             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
133     elif 0 != len(tokens):
134         print("Invalid command/argument, or bad number of tokens.")
135
136
137 def atomic_write(path, text, do_append=False):
138     """Atomic write of text to file at path, appended if do_append is set."""
139     path_tmp = path + io_db["tmp_suffix"]
140     mode = "w"
141     if do_append:
142         mode = "a"
143         if os.access(path, os.F_OK):
144             shutil.copyfile(path, path_tmp)
145     file = open(path_tmp, mode)
146     strong_write(file, text)
147     file.close()
148     if os.access(path, os.F_OK):
149         os.remove(path)
150     os.rename(path_tmp, path)
151
152
153 def record(command):
154     """Append command string plus newline to record file. (Atomic.)"""
155     # This misses some optimizations from the original record(), namely only
156     # finishing the atomic write with expensive flush() and fsync() every 15
157     # seconds unless explicitely forced. Implement as needed.
158     atomic_write(io_db["path_record"], command + "\n", do_append=True)
159
160
161 def save_world():
162     """Save all commands needed to reconstruct current world state."""
163     # TODO: Misses same optimizations as record() from the original record().
164
165     def quote(string):
166         string = string.replace("\u005C", '\u005C\u005C')
167         return '"' + string.replace('"', '\u005C"') + '"'
168
169     def mapsetter(key):
170         def helper(id):
171             string = ""
172             if world_db["Things"][id][key]:
173                 map = world_db["Things"][id][key]
174                 length = world_db["MAP_LENGTH"]
175                 for i in range(length):
176                     line = map[i * length:(i * length) + length].decode()
177                     string = string + key + " " + str(i) + " " + quote(line) \
178                              + "\n"
179             return string
180         return helper
181
182     def memthing(id):
183         string = ""
184         for memthing in world_db["Things"][id]["T_MEMTHING"]:
185             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
186                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
187         return string
188
189     def helper(category, id_string, special_keys={}):
190         string = ""
191         for id in world_db[category]:
192             string = string + id_string + " " + str(id) + "\n"
193             for key in world_db[category][id]:
194                 if not key in special_keys:
195                     x = world_db[category][id][key]
196                     argument = quote(x) if str == type(x) else str(x)
197                     string = string + key + " " + argument + "\n"
198                 elif special_keys[key]:
199                     string = string + special_keys[key](id)
200         return string
201
202     string = ""
203     for key in world_db:
204         if dict != type(world_db[key]) and key != "MAP":
205             string = string + key + " " + str(world_db[key]) + "\n"
206     string = string + helper("ThingActions", "TA_ID")
207     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
208     for id in world_db["ThingTypes"]:
209         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
210                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
211     string = string + helper("Things", "T_ID",
212                              {"T_CARRIES": False, "carried": False,
213                               "T_MEMMAP": mapsetter("T_MEMMAP"),
214                               "T_MEMTHING": memthing, "fovmap": False,
215                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
216     for id in world_db["Things"]:
217         if [] != world_db["Things"][id]["T_CARRIES"]:
218             string = string + "T_ID " + str(id) + "\n"
219             for carried_id in world_db["Things"][id]["T_CARRIES"]:
220                 string = string + "T_CARRIES " + str(carried_id) + "\n"
221     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
222              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
223     atomic_write(io_db["path_save"], string)
224
225
226 def obey_lines_in_file(path, name, do_record=False):
227     """Call obey() on each line of path's file, use name in input prefix."""
228     file = open(path, "r")
229     line_n = 1
230     for line in file.readlines():
231         obey(line.rstrip(), name + "file line " + str(line_n),
232              do_record=do_record)
233         line_n = line_n + 1
234     file.close()
235
236
237 def parse_command_line_arguments():
238     """Return settings values read from command line arguments."""
239     parser = argparse.ArgumentParser()
240     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
241                         action='store')
242     opts, unknown = parser.parse_known_args()
243     return opts
244
245
246 def server_test():
247     """Ensure valid server out file belonging to current process.
248
249     This is done by comparing io_db["teststring"] to what's found at the start
250     of the current file at io_db["path_out"]. On failure, set
251     io_db["kicked_by_rival"] and raise SystemExit.
252     """
253     if not os.access(io_db["path_out"], os.F_OK):
254         raise SystemExit("Server output file has disappeared.")
255     file = open(io_db["path_out"], "r")
256     test = file.readline().rstrip("\n")
257     file.close()
258     if test != io_db["teststring"]:
259         io_db["kicked_by_rival"] = True
260         msg = "Server test string in server output file does not match. This" \
261               " indicates that the current server process has been " \
262               "superseded by another one."
263         raise SystemExit(msg)
264
265
266 def read_command():
267     """Return next newline-delimited command from server in file.
268
269     Keep building return string until a newline is encountered. Pause between
270     unsuccessful reads, and after too much waiting, run server_test().
271     """
272     wait_on_fail = 0.03333
273     max_wait = 5
274     now = time.time()
275     command = ""
276     while True:
277         add = io_db["file_in"].readline()
278         if len(add) > 0:
279             command = command + add
280             if len(command) > 0 and "\n" == command[-1]:
281                 command = command[:-1]
282                 break
283         else:
284             time.sleep(wait_on_fail)
285             if now + max_wait < time.time():
286                 server_test()
287                 now = time.time()
288     return command
289
290
291 def try_worldstate_update():
292     """Write worldstate file if io_db["worldstate_updateable"] is set."""
293     if io_db["worldstate_updateable"]:
294
295         def draw_visible_Things(map, run):
296             for id in world_db["Things"]:
297                 type = world_db["Things"][id]["T_TYPE"]
298                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
299                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
300                 if (0 == run and not consumable and not alive) \
301                    or (1 == run and consumable and not alive) \
302                    or (2 == run and alive):
303                     y = world_db["Things"][id]["T_POSY"]
304                     x = world_db["Things"][id]["T_POSX"]
305                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
306                     if 'v' == chr(fovflag):
307                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
308                         map[(y * length) + x] = ord(c)
309
310         def write_map(string, map):
311             for i in range(length):
312                 line = map[i * length:(i * length) + length].decode()
313                 string = string + line + "\n"
314             return string
315
316         inventory = ""
317         if [] == world_db["Things"][0]["T_CARRIES"]:
318             inventory = "(none)\n"
319         else:
320             for id in world_db["Things"][0]["T_CARRIES"]:
321                 type_id = world_db["Things"][id]["T_TYPE"]
322                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
323                 inventory = inventory + name + "\n"
324         string = str(world_db["TURN"]) + "\n" + \
325                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
326                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
327                  inventory + "%\n" + \
328                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
329                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
330                  str(world_db["MAP_LENGTH"]) + "\n"
331         length = world_db["MAP_LENGTH"]
332         fov = bytearray(b' ' * (length ** 2))
333         for pos in range(length ** 2):
334             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
335                 fov[pos] = world_db["MAP"][pos]
336         for i in range(3):
337             draw_visible_Things(fov, i)
338         string = write_map(string, fov)
339         mem = world_db["Things"][0]["T_MEMMAP"][:]
340         for i in range(2):
341             for memthing in world_db["Things"][0]["T_MEMTHING"]:
342                 type = world_db["Things"][memthing[0]]["T_TYPE"]
343                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
344                 if (i == 0 and not consumable) or (i == 1 and consumable):
345                     c = world_db["ThingTypes"][type]["TT_SYMBOL"]
346                     mem[(memthing[1] * length) + memthing[2]] = ord(c)
347         string = write_map(string, mem)
348         atomic_write(io_db["path_worldstate"], string)
349         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
350         io_db["worldstate_updateable"] = False
351
352
353 def replay_game():
354     """Replay game from record file.
355
356     Use opts.replay as breakpoint turn to which to replay automatically before
357     switching to manual input by non-meta commands in server input file
358     triggering further reads of record file. Ensure opts.replay is at least 1.
359     Run try_worldstate_update() before each interactive obey()/read_command().
360     """
361     if opts.replay < 1:
362         opts.replay = 1
363     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
364           " (if so late a turn is to be found).")
365     if not os.access(io_db["path_record"], os.F_OK):
366         raise SystemExit("No record file found to replay.")
367     io_db["file_record"] = open(io_db["path_record"], "r")
368     io_db["file_record"].prefix = "record file line "
369     io_db["file_record"].line_n = 1
370     while world_db["TURN"] < opts.replay:
371         line = io_db["file_record"].readline()
372         if "" == line:
373             break
374         obey(line.rstrip(), io_db["file_record"].prefix
375              + str(io_db["file_record"].line_n))
376         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
377     while True:
378         try_worldstate_update()
379         obey(read_command(), "in file", replay=True)
380
381
382 def play_game():
383     """Play game by server input file commands. Before, load save file found.
384
385     If no save file is found, a new world is generated from the commands in the
386     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
387     command and all that follow via the server input file. Run
388     try_worldstate_update() before each interactive obey()/read_command().
389     """
390     if os.access(io_db["path_save"], os.F_OK):
391         obey_lines_in_file(io_db["path_save"], "save")
392     else:
393         if not os.access(io_db["path_worldconf"], os.F_OK):
394             msg = "No world config file from which to start a new world."
395             raise SystemExit(msg)
396         obey_lines_in_file(io_db["path_worldconf"], "world config ",
397                            do_record=True)
398         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
399     while True:
400         try_worldstate_update()
401         obey(read_command(), "in file", do_record=True)
402
403
404 def remake_map():
405     """(Re-)make island map.
406
407     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
408     start with one land cell in the middle, then go into cycle of repeatedly
409     selecting a random sea cell and transforming it into land if it is neighbor
410     to land. The cycle ends when a land cell is due to be created at the map's
411     border. Then put some trees on the map (TODO: more precise algorithm desc).
412     """
413     def is_neighbor(coordinates, type):
414         y = coordinates[0]
415         x = coordinates[1]
416         length = world_db["MAP_LENGTH"]
417         ind = y % 2
418         diag_west = x + (ind > 0)
419         diag_east = x + (ind < (length - 1))
420         pos = (y * length) + x
421         if (y > 0 and diag_east
422             and type == chr(world_db["MAP"][pos - length + ind])) \
423            or (x < (length - 1)
424                and type == chr(world_db["MAP"][pos + 1])) \
425            or (y < (length - 1) and diag_east
426                and type == chr(world_db["MAP"][pos + length + ind])) \
427            or (y > 0 and diag_west
428                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
429            or (x > 0
430                and type == chr(world_db["MAP"][pos - 1])) \
431            or (y < (length - 1) and diag_west
432                and type == chr(world_db["MAP"][pos + length - (not ind)])):
433             return True
434         return False
435     store_seed = rand.seed
436     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
437     length = world_db["MAP_LENGTH"]
438     add_half_width = (not (length % 2)) * int(length / 2)
439     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
440     while (1):
441         y = rand.next() % length
442         x = rand.next() % length
443         pos = (y * length) + x
444         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
445             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
446                 break
447             world_db["MAP"][pos] = ord(".")
448     n_trees = int((length ** 2) / 16)
449     i_trees = 0
450     while (i_trees <= n_trees):
451         single_allowed = rand.next() % 32
452         y = rand.next() % length
453         x = rand.next() % length
454         pos = (y * length) + x
455         if "." == chr(world_db["MAP"][pos]) \
456           and ((not single_allowed) or is_neighbor((y, x), "X")):
457             world_db["MAP"][pos] = ord("X")
458             i_trees += 1
459     rand.seed = store_seed
460     # This all-too-precise replica of the original C code misses iter_limit().
461
462
463 def update_map_memory(t):
464     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
465     if not t["T_MEMMAP"]:
466         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
467     if not t["T_MEMDEPTHMAP"]:
468         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
469     for pos in range(world_db["MAP_LENGTH"] ** 2):
470         if "v" == chr(t["fovmap"][pos]):
471             t["T_MEMDEPTHMAP"][pos] = ord("0")
472             if " " == chr(t["T_MEMMAP"][pos]):
473                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
474             continue
475         # TODO: Aging of MEMDEPTHMAP.
476     for memthing in t["T_MEMTHING"]:
477         y = world_db["Things"][memthing[0]]["T_POSY"]
478         x = world_db["Things"][memthing[0]]["T_POSX"]
479         if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
480             t["T_MEMTHING"].remove(memthing)
481     for id in world_db["Things"]:
482         type = world_db["Things"][id]["T_TYPE"]
483         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
484             y = world_db["Things"][id]["T_POSY"]
485             x = world_db["Things"][id]["T_POSY"]
486             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
487                 t["T_MEMTHING"].append((type, y, x))
488
489
490 def set_world_inactive():
491     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
492     server_test()
493     if os.access(io_db["path_worldstate"], os.F_OK):
494         os.remove(io_db["path_worldstate"])
495     world_db["WORLD_ACTIVE"] = 0
496
497
498 def integer_test(val_string, min, max):
499     """Return val_string if possible integer >= min and <= max, else None."""
500     try:
501         val = int(val_string)
502         if val < min or val > max:
503             raise ValueError
504         return val
505     except ValueError:
506         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
507               str(max) + ".")
508         return None
509
510
511 def setter(category, key, min, max):
512     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
513     if category is None:
514         def f(val_string):
515             val = integer_test(val_string, min, max)
516             if None != val:
517                 world_db[key] = val
518     else:
519         if category == "Thing":
520             id_store = command_tid
521             decorator = test_Thing_id
522         elif category == "ThingType":
523             id_store = command_ttid
524             decorator = test_ThingType_id
525         elif category == "ThingAction":
526             id_store = command_taid
527             decorator = test_ThingAction_id
528
529         @decorator
530         def f(val_string):
531             val = integer_test(val_string, min, max)
532             if None != val:
533                 world_db[category + "s"][id_store.id][key] = val
534     return f
535
536
537 def build_fov_map(t):
538     """Build Thing's FOV map."""
539     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
540     # DUMMY so far. Just builds an all-visible map.
541
542
543 def decrement_lifepoints(t):
544     """Decrement t's lifepoints by 1, and if to zero, corpse it.
545
546     If t is the player avatar, only blank its fovmap, so that the client may
547     still display memory data. On non-player things, erase fovmap and memory.
548     """
549     t["T_LIFEPOINTS"] -= 1
550     if 0 == t["T_LIFEPOINTS"]:
551         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
552         if world_db["Things"][0] == t:
553             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
554             strong_write(io_db["file_out"], "LOG You die.\n")
555         else:
556             t["fovmap"] = False
557             t["T_MEMMAP"] = False
558             t["T_MEMDEPTHMAP"] = False
559             t["T_MEMTHING"] = []
560             strong_write(io_db["file_out"], "LOG It dies.\n")
561
562
563 def actor_wait(t):
564     """Make t do nothing (but loudly, if player avatar)."""
565     if t == world_db["Things"][0]:
566         strong_write(io_db["file_out"], "LOG You wait.\n")
567
568
569 def actor_move(t):
570     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
571     dir_c = t["T_ARGUMENT"].encode("ascii")[0]
572     legal_move = libpr.mv_yx_in_dir_legal_wrap(dir_c, t["T_POSY"], t["T_POSX"])
573     passable = False
574     if -1 == legal_move:
575         raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
576     elif 1 == legal_move:
577         pos = (libpr.result_y() * world_db["MAP_LENGTH"]) + libpr.result_x()
578         passable = "." == chr(world_db["MAP"][pos])
579         hitted = [id for id in world_db["Things"]
580                   if world_db["Things"][id] != t
581                   if world_db["Things"][id]["T_LIFEPOINTS"]
582                   if world_db["Things"][id]["T_POSY"] == libpr.result_y()
583                   if world_db["Things"][id]["T_POSX"] == libpr.result_x()]
584         if len(hitted):
585             hitted = hitted[0]
586             decrement_lifepoints(world_db["Things"][hitted])
587             hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
588             hitter = "You" if t == world_db["Things"][0] else hitter_name
589             hitted_type = world_db["Things"][hitted]["T_TYPE"]
590             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
591             hitted = "you" if hitted == world_db["Things"][0] else hitted_name
592             verb = " wound " if hitter == "You" else " wounds "
593             strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
594                                             ".\n")
595             return
596     dir = [dir for dir in directions_db
597            if directions_db[dir] == t["T_ARGUMENT"]][0]
598     if passable:
599         t["T_POSY"] = libpr.result_y()
600         t["T_POSX"] = libpr.result_x()
601         for id in t["T_CARRIES"]:
602             world_db["Things"][id]["T_POSY"] = libpr.result_y()
603             world_db["Things"][id]["T_POSX"] = libpr.result_x()
604         build_fov_map(t)
605         strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
606     else:
607         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
608
609
610 def actor_pick_up(t):
611     """Make t pick up (topmost?) Thing from ground into inventory."""
612     # Topmostness is actually not defined so far.
613     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
614            if not world_db["Things"][id]["carried"]
615            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
616            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
617     if len(ids):
618         world_db["Things"][ids[0]]["carried"] = True
619         t["T_CARRIES"].append(ids[0])
620         if t == world_db["Things"][0]:
621             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
622     elif t == world_db["Things"][0]:
623         err = "You try to pick up an object, but there is none."
624         strong_write(io_db["file_out"], "LOG " + err + "\n")
625
626
627 def actor_drop(t):
628     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
629     # TODO: Handle case where T_ARGUMENT matches nothing.
630     if len(t["T_CARRIES"]):
631         id = t["T_CARRIES"][t["T_ARGUMENT"]]
632         t["T_CARRIES"].remove(id)
633         world_db["Things"][id]["carried"] = False
634         if t == world_db["Things"][0]:
635             strong_write(io_db["file_out"], "LOG You drop an object.\n")
636     elif t == world_db["Things"][0]:
637         err = "You try to drop an object, but you own none."
638         strong_write(io_db["file_out"], "LOG " + err + "\n")
639
640
641 def actor_use(t):
642     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
643     # Original wrongly featured lifepoints increase through consumable!
644     # TODO: Handle case where T_ARGUMENT matches nothing.
645     if len(t["T_CARRIES"]):
646         id = t["T_CARRIES"][t["T_ARGUMENT"]]
647         type = world_db["Things"][id]["T_TYPE"]
648         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
649             t["T_CARRIES"].remove(id)
650             del world_db["Things"][id]
651             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
652             strong_write(io_db["file_out"], "LOG You consume this object.\n")
653         else:
654             strong_write(io_db["file_out"], "LOG You try to use this object," +
655                                             "but fail.\n")
656     else:
657         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
658                                         "you own none.\n")
659
660
661 def turn_over():
662     """Run game world and its inhabitants until new player input expected."""
663     id = 0
664     whilebreaker = False
665     while world_db["Things"][0]["T_LIFEPOINTS"]:
666         for id in [id for id in world_db["Things"]
667                    if world_db["Things"][id]["T_LIFEPOINTS"]]:
668             Thing = world_db["Things"][id]
669             if Thing["T_LIFEPOINTS"]:
670                 if not Thing["T_COMMAND"]:
671                     update_map_memory(Thing)
672                     if 0 == id:
673                         whilebreaker = True
674                         break
675                     # DUMMY: ai(thing)
676                     Thing["T_COMMAND"] = 1
677                 # DUMMY: try_healing
678                 Thing["T_PROGRESS"] += 1
679                 taid = [a for a in world_db["ThingActions"]
680                           if a == Thing["T_COMMAND"]][0]
681                 ThingAction = world_db["ThingActions"][taid]
682                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
683                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
684                     Thing["T_COMMAND"] = 0
685                     Thing["T_PROGRESS"] = 0
686                 # DUMMY: hunger
687             # DUMMY: thingproliferation
688         if whilebreaker:
689             break
690         world_db["TURN"] += 1
691
692
693 def new_Thing(type, pos=(0,0)):
694     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
695     thing = {
696         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
697         "T_ARGUMENT": 0,
698         "T_PROGRESS": 0,
699         "T_SATIATION": 0,
700         "T_COMMAND": 0,
701         "T_TYPE": type,
702         "T_POSY": pos[0],
703         "T_POSX": pos[1],
704         "T_CARRIES": [],
705         "carried": False,
706         "T_MEMTHING": [],
707         "T_MEMMAP": False,
708         "T_MEMDEPTHMAP": False,
709         "fovmap": False
710     }
711     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
712         build_fov_map(thing)
713     return thing
714
715
716 def id_setter(id, category, id_store=False, start_at_1=False):
717     """Set ID of object of category to manipulate ID unused? Create new one.
718
719     The ID is stored as id_store.id (if id_store is set). If the integer of the
720     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
721     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
722     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
723     new object is created, otherwise the new object's ID.
724     """
725     min = 0 if start_at_1 else -32768
726     max = 255 if start_at_1 else 32767
727     if str == type(id):
728         id = integer_test(id, min, max)
729     if None != id:
730         if id in world_db[category]:
731             if id_store:
732                 id_store.id = id
733             return None
734         else:
735             if (start_at_1 and 0 == id) \
736                or ((not start_at_1) and (id < 0 or id > 255)):
737                 id = -1
738                 while 1:
739                     id = id + 1
740                     if id not in world_db[category]:
741                         break
742                 if id > 255:
743                     print("Ignoring: "
744                           "No unused ID available to add to ID list.")
745                     return None
746             if id_store:
747                 id_store.id = id
748     return id
749
750
751 def command_ping():
752     """Send PONG line to server output file."""
753     strong_write(io_db["file_out"], "PONG\n")
754
755
756 def command_quit():
757     """Abort server process."""
758     raise SystemExit("received QUIT command")
759
760
761 def command_thingshere(str_y, str_x):
762     """Write to out file list of Things known to player at coordinate y, x."""
763     def write_thing_if_here():
764         if y == world_db["Things"][id]["T_POSY"] \
765            and x == world_db["Things"][id]["T_POSX"] \
766            and not world_db["Things"][id]["carried"]:
767             type = world_db["Things"][id]["T_TYPE"]
768             name = world_db["ThingTypes"][type]["TT_NAME"]
769             strong_write(io_db["file_out"], name + "\n")
770     if world_db["WORLD_ACTIVE"]:
771         y = integer_test(str_y, 0, 255)
772         x = integer_test(str_x, 0, 255)
773         length = world_db["MAP_LENGTH"]
774         if None != y and None != x and y < length and x < length:
775             pos = (y * world_db["MAP_LENGTH"]) + x
776             strong_write(io_db["file_out"], "THINGS_HERE START\n")
777             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
778                 for id in world_db["Things"]:
779                     write_thing_if_here()
780             else:
781                 for id in world_db["Things"]["T_MEMTHING"]:
782                     write_thing_if_here()
783             strong_write(io_db["file_out"], "THINGS_HERE END\n")
784         else:
785             print("Ignoring: Invalid map coordinates.")
786     else:
787         print("Ignoring: Command only works on existing worlds.")
788
789
790 def play_commander(action, args=False):
791     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
792
793     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
794     """
795
796     def set_command():
797         id = [x for x in world_db["ThingActions"]
798                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
799         world_db["Things"][0]["T_COMMAND"] = id
800         turn_over()
801
802     def set_command_and_argument_int(str_arg):
803         val = integer_test(str_arg, 0, 255)
804         if None != val:
805             world_db["Things"][0]["T_ARGUMENT"] = val
806             set_command()
807
808     def set_command_and_argument_movestring(str_arg):
809         if str_arg in directions_db:
810             world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
811             set_command()
812         else:
813             print("Ignoring: Argument must be valid direction string.")
814
815     if action == "move":
816         return set_command_and_argument_movestring
817     elif args:
818         return set_command_and_argument_int
819     else:
820         return set_command
821
822
823 def command_seedrandomness(seed_string):
824     """Set rand seed to int(seed_string)."""
825     val = integer_test(seed_string, 0, 4294967295)
826     if None != val:
827         rand.seed = val
828
829
830 def command_seedmap(seed_string):
831     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
832     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
833     remake_map()
834
835
836 def command_makeworld(seed_string):
837     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
838
839     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
840     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
841     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
842     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
843     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
844     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
845     other. Init player's memory map. Write "NEW_WORLD" line to out file.
846     """
847
848     def free_pos():
849         i = 0
850         while 1:
851             err = "Space to put thing on too hard to find. Map too small?"
852             while 1:
853                 y = rand.next() % world_db["MAP_LENGTH"]
854                 x = rand.next() % world_db["MAP_LENGTH"]
855                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
856                     break
857                 i += 1
858                 if i == 65535:
859                     raise SystemExit(err)
860             # Replica of C code, wrongly ignores animatedness of new Thing.
861             pos_clear = (0 == len([id for id in world_db["Things"]
862                                    if world_db["Things"][id]["T_LIFEPOINTS"]
863                                    if world_db["Things"][id]["T_POSY"] == y
864                                    if world_db["Things"][id]["T_POSX"] == x]))
865             if pos_clear:
866                 break
867         return (y, x)
868
869     val = integer_test(seed_string, 0, 4294967295)
870     if None == val:
871         return
872     rand.seed = val
873     world_db["SEED_MAP"] = val
874     player_will_be_generated = False
875     playertype = world_db["PLAYER_TYPE"]
876     for ThingType in world_db["ThingTypes"]:
877         if playertype == ThingType:
878             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
879                 player_will_be_generated = True
880             break
881     if not player_will_be_generated:
882         print("Ignoring beyond SEED_MAP: " +
883               "No player type with start number >0 defined.")
884         return
885     wait_action = False
886     for ThingAction in world_db["ThingActions"]:
887         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
888             wait_action = True
889     if not wait_action:
890         print("Ignoring beyond SEED_MAP: " +
891               "No thing action with name 'wait' defined.")
892         return
893     world_db["Things"] = {}
894     remake_map()
895     world_db["WORLD_ACTIVE"] = 1
896     world_db["TURN"] = 1
897     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
898         id = id_setter(-1, "Things")
899         world_db["Things"][id] = new_Thing(playertype, free_pos())
900     update_map_memory(world_db["Things"][0])
901     for type in world_db["ThingTypes"]:
902         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
903             if type != playertype:
904                 id = id_setter(-1, "Things")
905                 world_db["Things"][id] = new_Thing(type, free_pos())
906     strong_write(io_db["file_out"], "NEW_WORLD\n")
907
908
909 def command_maplength(maplength_string):
910     """Redefine map length. Invalidate map, therefore lose all things on it."""
911     val = integer_test(maplength_string, 1, 256)
912     if None != val:
913         world_db["MAP_LENGTH"] = val
914         set_world_inactive()
915         world_db["Things"] = {}
916         libpr.set_maplength(val)
917
918
919 def command_worldactive(worldactive_string):
920     """Toggle world_db["WORLD_ACTIVE"] if possible.
921
922     An active world can always be set inactive. An inactive world can only be
923     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
924     activation, rebuild all Things' FOVs, and the player's map memory.
925     """
926     # In original version, map existence was also tested (unnecessarily?).
927     val = integer_test(worldactive_string, 0, 1)
928     if val:
929         if 0 != world_db["WORLD_ACTIVE"]:
930             if 0 == val:
931                 set_world_inactive()
932             else:
933                 print("World already active.")
934         elif 0 == world_db["WORLD_ACTIVE"]:
935             wait_exists = False
936             for ThingAction in world_db["ThingActions"]:
937                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
938                     wait_exists = True
939                     break
940             player_exists = False
941             for Thing in world_db["Things"]:
942                 if 0 == Thing:
943                     player_exists = True
944                     break
945             if wait_exists and player_exists:
946                 for id in world_db["Things"]:
947                     if world_db["Things"][id]["T_LIFEPOINTS"]:
948                         build_fov_map(world_db["Things"][id])
949                         if 0 == id:
950                             update_map_memory(world_db["Things"][id])
951                 world_db["WORLD_ACTIVE"] = 1
952
953
954 def test_for_id_maker(object, category):
955     """Return decorator testing for object having "id" attribute."""
956     def decorator(f):
957         def helper(*args):
958             if hasattr(object, "id"):
959                 f(*args)
960             else:
961                 print("Ignoring: No " + category +
962                       " defined to manipulate yet.")
963         return helper
964     return decorator
965
966
967 def command_tid(id_string):
968     """Set ID of Thing to manipulate. ID unused? Create new one.
969
970     Default new Thing's type to the first available ThingType, others: zero.
971     """
972     id = id_setter(id_string, "Things", command_tid)
973     if None != id:
974         if world_db["ThingTypes"] == {}:
975             print("Ignoring: No ThingType to settle new Thing in.")
976             return
977         type = list(world_db["ThingTypes"].keys())[0]
978         world_db["Things"][id] = new_Thing(type)
979
980
981 test_Thing_id = test_for_id_maker(command_tid, "Thing")
982
983
984 @test_Thing_id
985 def command_tcommand(str_int):
986     """Set T_COMMAND of selected Thing."""
987     val = integer_test(str_int, 0, 255)
988     if None != val:
989         if 0 == val or val in world_db["ThingActions"]:
990             world_db["Things"][command_tid.id]["T_COMMAND"] = val
991         else:
992             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
993
994
995 @test_Thing_id
996 def command_ttype(str_int):
997     """Set T_TYPE of selected Thing."""
998     val = integer_test(str_int, 0, 255)
999     if None != val:
1000         if val in world_db["ThingTypes"]:
1001             world_db["Things"][command_tid.id]["T_TYPE"] = val
1002         else:
1003             print("Ignoring: ThingType ID belongs to no known ThingType.")
1004
1005
1006 @test_Thing_id
1007 def command_tcarries(str_int):
1008     """Append int(str_int) to T_CARRIES of selected Thing.
1009
1010     The ID int(str_int) must not be of the selected Thing, and must belong to a
1011     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1012     """
1013     val = integer_test(str_int, 0, 255)
1014     if None != val:
1015         if val == command_tid.id:
1016             print("Ignoring: Thing cannot carry itself.")
1017         elif val in world_db["Things"] \
1018              and not world_db["Things"][val]["carried"]:
1019             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1020             world_db["Things"][val]["carried"] = True
1021         else:
1022             print("Ignoring: Thing not available for carrying.")
1023     # Note that the whole carrying structure is different from the C version:
1024     # Carried-ness is marked by a "carried" flag, not by Things containing
1025     # Things internally.
1026
1027
1028 @test_Thing_id
1029 def command_tmemthing(str_t, str_y, str_x):
1030     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1031
1032     The type must fit to an existing ThingType, and the position into the map.
1033     """
1034     type = integer_test(str_t, 0, 255)
1035     posy = integer_test(str_y, 0, 255)
1036     posx = integer_test(str_x, 0, 255)
1037     if None != type and None != posy and None != posx:
1038         if type not in world_db["ThingTypes"] \
1039            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1040             print("Ignoring: Illegal value for thing type or position.")
1041         else:
1042             memthing = (type, posy, posx)
1043             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1044
1045
1046 def setter_map(maptype):
1047     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1048
1049     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1050     """
1051     @test_Thing_id
1052     def helper(str_int, mapline):
1053         val = integer_test(str_int, 0, 255)
1054         if None != val:
1055             if val >= world_db["MAP_LENGTH"]:
1056                 print("Illegal value for map line number.")
1057             elif len(mapline) != world_db["MAP_LENGTH"]:
1058                 print("Map line length is unequal map width.")
1059             else:
1060                 length = world_db["MAP_LENGTH"]
1061                 map = None
1062                 if not world_db["Things"][command_tid.id][maptype]:
1063                     map = bytearray(b' ' * (length ** 2))
1064                 else:
1065                     map = world_db["Things"][command_tid.id][maptype]
1066                 map[val * length:(val * length) + length] = mapline.encode()
1067                 world_db["Things"][command_tid.id][maptype] = map
1068     return helper
1069
1070
1071 def setter_tpos(axis):
1072     """Generate setter for T_POSX or  T_POSY of selected Thing.
1073
1074     If world is active, rebuilds animate things' fovmap, player's memory map.
1075     """
1076     @test_Thing_id
1077     def helper(str_int):
1078         val = integer_test(str_int, 0, 255)
1079         if None != val:
1080             if val < world_db["MAP_LENGTH"]:
1081                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1082                 if world_db["WORLD_ACTIVE"] \
1083                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1084                     build_fov_map(world_db["Things"][command_tid.id])
1085                     if 0 == command_tid.id:
1086                         update_map_memory(world_db["Things"][command_tid.id])
1087             else:
1088                 print("Ignoring: Position is outside of map.")
1089     return helper
1090
1091
1092 def command_ttid(id_string):
1093     """Set ID of ThingType to manipulate. ID unused? Create new one.
1094
1095     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1096     """
1097     id = id_setter(id_string, "ThingTypes", command_ttid)
1098     if None != id:
1099         world_db["ThingTypes"][id] = {
1100             "TT_NAME": "(none)",
1101             "TT_CONSUMABLE": 0,
1102             "TT_LIFEPOINTS": 0,
1103             "TT_PROLIFERATE": 0,
1104             "TT_START_NUMBER": 0,
1105             "TT_SYMBOL": "?",
1106             "TT_CORPSE_ID": id
1107         }
1108
1109
1110 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1111
1112
1113 @test_ThingType_id
1114 def command_ttname(name):
1115     """Set TT_NAME of selected ThingType."""
1116     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1117
1118
1119 @test_ThingType_id
1120 def command_ttsymbol(char):
1121     """Set TT_SYMBOL of selected ThingType. """
1122     if 1 == len(char):
1123         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1124     else:
1125         print("Ignoring: Argument must be single character.")
1126
1127
1128 @test_ThingType_id
1129 def command_ttcorpseid(str_int):
1130     """Set TT_CORPSE_ID of selected ThingType."""
1131     val = integer_test(str_int, 0, 255)
1132     if None != val:
1133         if val in world_db["ThingTypes"]:
1134             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1135         else:
1136             print("Ignoring: Corpse ID belongs to no known ThignType.")
1137
1138
1139 def command_taid(id_string):
1140     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1141
1142     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1143     """
1144     id = id_setter(id_string, "ThingActions", command_taid, True)
1145     if None != id:
1146         world_db["ThingActions"][id] = {
1147             "TA_EFFORT": 1,
1148             "TA_NAME": "wait"
1149         }
1150
1151
1152 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1153
1154
1155 @test_ThingAction_id
1156 def command_taname(name):
1157     """Set TA_NAME of selected ThingAction.
1158
1159     The name must match a valid thing action function. If after the name
1160     setting no ThingAction with name "wait" remains, call set_world_inactive().
1161     """
1162     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1163        or name == "pick_up":
1164         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1165         if 1 == world_db["WORLD_ACTIVE"]:
1166             wait_defined = False
1167             for id in world_db["ThingActions"]:
1168                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1169                     wait_defined = True
1170                     break
1171             if not wait_defined:
1172                 set_world_inactive()
1173     else:
1174         print("Ignoring: Invalid action name.")
1175     # In contrast to the original,naming won't map a function to a ThingAction.
1176
1177
1178 """Commands database.
1179
1180 Map command start tokens to ([0]) number of expected command arguments, ([1])
1181 the command's meta-ness (i.e. is it to be written to the record file, is it to
1182 be ignored in replay mode if read from server input file), and ([2]) a function
1183 to be called on it.
1184 """
1185 commands_db = {
1186     "QUIT": (0, True, command_quit),
1187     "PING": (0, True, command_ping),
1188     "THINGS_HERE": (2, True, command_thingshere),
1189     "MAKE_WORLD": (1, False, command_makeworld),
1190     "SEED_MAP": (1, False, command_seedmap),
1191     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1192     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1193     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1194     "MAP_LENGTH": (1, False, command_maplength),
1195     "WORLD_ACTIVE": (1, False, command_worldactive),
1196     "TA_ID": (1, False, command_taid),
1197     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1198     "TA_NAME": (1, False, command_taname),
1199     "TT_ID": (1, False, command_ttid),
1200     "TT_NAME": (1, False, command_ttname),
1201     "TT_SYMBOL": (1, False, command_ttsymbol),
1202     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1203     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1204                                        0, 65535)),
1205     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1206                                          0, 255)),
1207     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1208                                         0, 255)),
1209     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1210     "T_ID": (1, False, command_tid),
1211     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1212     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1213     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1214     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1215     "T_COMMAND": (1, False, command_tcommand),
1216     "T_TYPE": (1, False, command_ttype),
1217     "T_CARRIES": (1, False, command_tcarries),
1218     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1219     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1220     "T_MEMTHING": (3, False, command_tmemthing),
1221     "T_POSY": (1, False, setter_tpos("Y")),
1222     "T_POSX": (1, False, setter_tpos("X")),
1223     "wait": (0, False, play_commander("wait")),
1224     "move": (1, False, play_commander("move")),
1225     "pick_up": (0, False, play_commander("pick_up")),
1226     "drop": (1, False, play_commander("drop", True)),
1227     "use": (1, False, play_commander("use", True)),
1228 }
1229
1230
1231 """World state database. With sane default values. (Randomness is in rand.)"""
1232 world_db = {
1233     "TURN": 0,
1234     "SEED_MAP": 0,
1235     "PLAYER_TYPE": 0,
1236     "MAP_LENGTH": 64,
1237     "WORLD_ACTIVE": 0,
1238     "ThingActions": {},
1239     "ThingTypes": {},
1240     "Things": {}
1241 }
1242
1243 """Mapping of direction names to internal direction chars."""
1244 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1245                  "west": "s", "north-west": "w", "north-east": "e"}
1246
1247 """File IO database."""
1248 io_db = {
1249     "path_save": "save",
1250     "path_record": "record",
1251     "path_worldconf": "confserver/world",
1252     "path_server": "server/",
1253     "path_in": "server/in",
1254     "path_out": "server/out",
1255     "path_worldstate": "server/worldstate",
1256     "tmp_suffix": "_tmp",
1257     "kicked_by_rival": False,
1258     "worldstate_updateable": False
1259 }
1260
1261
1262 try:
1263     libpr = prep_library()
1264     rand = RandomnessIO()
1265     opts = parse_command_line_arguments()
1266     setup_server_io()
1267     if None != opts.replay:
1268         replay_game()
1269     else:
1270         play_game()
1271 except SystemExit as exit:
1272     print("ABORTING: " + exit.args[0])
1273 except:
1274     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1275     raise
1276 finally:
1277     cleanup_server_io()