home · contact · privacy
Client: Slightly grow wait time in test_and_poll_server().
[plomrogue] / roguelike-server
1 #!/usr/bin/python3
2
3 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
4 # or any later version. For details on its copyright, license, and warranties,
5 # see the file NOTICE in the root directory of the PlomRogue source package.
6
7
8 import argparse
9 import errno
10 import os
11 import shlex
12 import shutil
13 import time
14 import ctypes
15 import math
16
17
18 class RandomnessIO:
19     """"Interface to libplomrogue's pseudo-randomness generator."""
20
21     def set_seed(self, seed):
22         libpr.seed_rrand(1, seed)
23
24     def get_seed(self):
25         return libpr.seed_rrand(0, 0)
26
27     def next(self):
28         return libpr.rrand()
29
30     seed = property(get_seed, set_seed)
31
32
33 def prep_library():
34     """Prepare ctypes library at ./libplomrogue.so"""
35     libpath = ("./libplomrogue.so")
36     if not os.access(libpath, os.F_OK):
37         raise SystemExit("No library " + libpath + ", run ./redo first?")
38     libpr = ctypes.cdll.LoadLibrary(libpath)
39     libpr.seed_rrand.restype = ctypes.c_uint32
40     return libpr
41
42
43 def c_pointer_to_bytearray(ba):
44     """Return C char * pointer to ba."""
45     type = ctypes.c_char * len(ba)
46     return type.from_buffer(ba)
47
48
49 def strong_write(file, string):
50     """Apply write(string), then flush()."""
51     file.write(string)
52     file.flush()
53
54
55 def setup_server_io():
56     """Fill IO files DB with proper file( path)s. Write process IO test string.
57
58     Ensure IO files directory at server/. Remove any old input file if found.
59     Set up new input file for reading, and new output file for writing. Start
60     output file with process hash line of format PID + " " + floated UNIX time
61     (io_db["teststring"]). Raise SystemExit if file is found at path of either
62     record or save file plus io_db["tmp_suffix"].
63     """
64     def detect_atomic_leftover(path, tmp_suffix):
65         path_tmp = path + tmp_suffix
66         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
67               "aborted previous attempt to write '" + path + "'. Aborting " \
68               "until matter is resolved by removing it from its current path."
69         if os.access(path_tmp, os.F_OK):
70             raise SystemExit(msg)
71     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
72     io_db["save_wait"] = 0
73     io_db["verbose"] = False
74     io_db["record_chunk"] = ""
75     os.makedirs(io_db["path_server"], exist_ok=True)
76     io_db["file_out"] = open(io_db["path_out"], "w")
77     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
78     if os.access(io_db["path_in"], os.F_OK):
79         os.remove(io_db["path_in"])
80     io_db["file_in"] = open(io_db["path_in"], "w")
81     io_db["file_in"].close()
82     io_db["file_in"] = open(io_db["path_in"], "r")
83     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
84     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
85
86
87 def cleanup_server_io():
88     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
89     def helper(file_key, path_key):
90         if file_key in io_db:
91             io_db[file_key].close()
92         if not io_db["kicked_by_rival"] \
93            and os.access(io_db[path_key], os.F_OK):
94             os.remove(io_db[path_key])
95     helper("file_in", "path_in")
96     helper("file_out", "path_out")
97     helper("file_worldstate", "path_worldstate")
98     if "file_record" in io_db:
99         io_db["file_record"].close()
100
101
102 def log(msg):
103     """Send "msg" to log."""
104     strong_write(io_db["file_out"], "LOG " + msg + "\n")
105
106
107 def obey(command, prefix, replay=False, do_record=False):
108     """Call function from commands_db mapped to command's first token.
109
110     Tokenize command string with shlex.split(comments=True). If replay is set,
111     a non-meta command from the commands_db merely triggers obey() on the next
112     command from the records file. If not, non-meta commands set
113     io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
114     do_record is set, are recorded to io_db["record_chunk"], and save_world()
115     is called (and io_db["record_chunk"] written) if 15 seconds have passed
116     since the last time it was called. The prefix string is inserted into the
117     server's input message between its beginning 'input ' and ':'. All activity
118     is preceded by a server_test() call. Commands that start with a lowercase
119     letter are ignored when world_db["WORLD_ACTIVE"] is False/0.
120     """
121     server_test()
122     if io_db["verbose"]:
123         print("input " + prefix + ": " + command)
124     try:
125         tokens = shlex.split(command, comments=True)
126     except ValueError as err:
127         print("Can't tokenize command string: " + str(err) + ".")
128         return
129     if len(tokens) > 0 and tokens[0] in commands_db \
130        and len(tokens) == commands_db[tokens[0]][0] + 1:
131         if commands_db[tokens[0]][1]:
132             commands_db[tokens[0]][2](*tokens[1:])
133         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
134             print("Ignoring lowercase-starting commands when world inactive.")
135         elif replay:
136             print("Due to replay mode, reading command as 'go on in record'.")
137             line = io_db["file_record"].readline()
138             if len(line) > 0:
139                 obey(line.rstrip(), io_db["file_record"].prefix
140                      + str(io_db["file_record"].line_n))
141                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
142             else:
143                 print("Reached end of record file.")
144         else:
145             commands_db[tokens[0]][2](*tokens[1:])
146             if do_record:
147                 io_db["record_chunk"] += command + "\n"
148                 if time.time() > io_db["save_wait"] + 15:
149                     atomic_write(io_db["path_record"], io_db["record_chunk"],
150                                  do_append=True)
151                     if world_db["WORLD_ACTIVE"]:
152                         save_world()
153                     io_db["record_chunk"] = ""
154                     io_db["save_wait"] = time.time()
155             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
156     elif 0 != len(tokens):
157         print("Invalid command/argument, or bad number of tokens.")
158
159
160 def atomic_write(path, text, do_append=False, delete=True):
161     """Atomic write of text to file at path, appended if do_append is set."""
162     path_tmp = path + io_db["tmp_suffix"]
163     mode = "w"
164     if do_append:
165         mode = "a"
166         if os.access(path, os.F_OK):
167             shutil.copyfile(path, path_tmp)
168     file = open(path_tmp, mode)
169     strong_write(file, text)
170     file.close()
171     if delete and os.access(path, os.F_OK):
172         os.remove(path)
173     os.rename(path_tmp, path)
174
175
176 def save_world():
177     """Save all commands needed to reconstruct current world state."""
178
179     def quote(string):
180         string = string.replace("\u005C", '\u005C\u005C')
181         return '"' + string.replace('"', '\u005C"') + '"'
182
183     def mapsetter(key):
184         def helper(id=None):
185             string = ""
186             if key == "MAP" or world_db["Things"][id][key]:
187                 map = world_db["MAP"] if key == "MAP" \
188                                       else world_db["Things"][id][key]
189                 length = world_db["MAP_LENGTH"]
190                 for i in range(length):
191                     line = map[i * length:(i * length) + length].decode()
192                     string = string + key + " " + str(i) + " " + quote(line) \
193                         + "\n"
194             return string
195         return helper
196
197     def memthing(id):
198         string = ""
199         for memthing in world_db["Things"][id]["T_MEMTHING"]:
200             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
201                 str(memthing[1]) + " " + str(memthing[2]) + "\n"
202         return string
203
204     def helper(category, id_string, special_keys={}):
205         string = ""
206         for id in sorted(world_db[category].keys()):
207             string = string + id_string + " " + str(id) + "\n"
208             for key in sorted(world_db[category][id].keys()):
209                 if not key in special_keys:
210                     x = world_db[category][id][key]
211                     argument = quote(x) if str == type(x) else str(x)
212                     string = string + key + " " + argument + "\n"
213                 elif special_keys[key]:
214                     string = string + special_keys[key](id)
215         return string
216
217     string = ""
218     for key in sorted(world_db.keys()):
219         if (not isinstance(world_db[key], dict)) and key != "MAP" and \
220            key != "WORLD_ACTIVE":
221             string = string + key + " " + str(world_db[key]) + "\n"
222     string = string + mapsetter("MAP")()
223     string = string + helper("ThingActions", "TA_ID")
224     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
225     for id in sorted(world_db["ThingTypes"].keys()):
226         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
227             str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
228     string = string + helper("Things", "T_ID",
229                              {"T_CARRIES": False, "carried": False,
230                               "T_MEMMAP": mapsetter("T_MEMMAP"),
231                               "T_MEMTHING": memthing, "fovmap": False,
232                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
233     for id in sorted(world_db["Things"].keys()):
234         if [] != world_db["Things"][id]["T_CARRIES"]:
235             string = string + "T_ID " + str(id) + "\n"
236             for carried in sorted(world_db["Things"][id]["T_CARRIES"]):
237                 string = string + "T_CARRIES " + str(carried) + "\n"
238     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
239         "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
240     atomic_write(io_db["path_save"], string)
241
242
243 def obey_lines_in_file(path, name, do_record=False):
244     """Call obey() on each line of path's file, use name in input prefix."""
245     file = open(path, "r")
246     line_n = 1
247     for line in file.readlines():
248         obey(line.rstrip(), name + "file line " + str(line_n),
249              do_record=do_record)
250         line_n = line_n + 1
251     file.close()
252
253
254 def parse_command_line_arguments():
255     """Return settings values read from command line arguments."""
256     parser = argparse.ArgumentParser()
257     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
258                         action='store')
259     parser.add_argument('-l', nargs="?", const="save", dest='savefile',
260                         action="store")
261     parser.add_argument('-v', dest='verbose', action='store_true')
262     opts, unknown = parser.parse_known_args()
263     return opts
264
265
266 def server_test():
267     """Ensure valid server out file belonging to current process.
268
269     This is done by comparing io_db["teststring"] to what's found at the start
270     of the current file at io_db["path_out"]. On failure, set
271     io_db["kicked_by_rival"] and raise SystemExit.
272     """
273     if not os.access(io_db["path_out"], os.F_OK):
274         raise SystemExit("Server output file has disappeared.")
275     file = open(io_db["path_out"], "r")
276     test = file.readline().rstrip("\n")
277     file.close()
278     if test != io_db["teststring"]:
279         io_db["kicked_by_rival"] = True
280         msg = "Server test string in server output file does not match. This" \
281               " indicates that the current server process has been " \
282               "superseded by another one."
283         raise SystemExit(msg)
284
285
286 def read_command():
287     """Return next newline-delimited command from server in file.
288
289     Keep building return string until a newline is encountered. Pause between
290     unsuccessful reads, and after too much waiting, run server_test().
291     """
292     wait_on_fail = 0.03333
293     max_wait = 5
294     now = time.time()
295     command = ""
296     while True:
297         add = io_db["file_in"].readline()
298         if len(add) > 0:
299             command = command + add
300             if len(command) > 0 and "\n" == command[-1]:
301                 command = command[:-1]
302                 break
303         else:
304             time.sleep(wait_on_fail)
305             if now + max_wait < time.time():
306                 server_test()
307                 now = time.time()
308     return command
309
310
311 def try_worldstate_update():
312     """Write worldstate file if io_db["worldstate_updateable"] is set."""
313     if io_db["worldstate_updateable"]:
314
315         def write_map(string, map):
316             for i in range(length):
317                 line = map[i * length:(i * length) + length].decode()
318                 string = string + line + "\n"
319             return string
320
321         inventory = ""
322         if [] == world_db["Things"][0]["T_CARRIES"]:
323             inventory = "(none)\n"
324         else:
325             for id in world_db["Things"][0]["T_CARRIES"]:
326                 type_id = world_db["Things"][id]["T_TYPE"]
327                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
328                 inventory = inventory + name + "\n"
329         string = str(world_db["TURN"]) + "\n" + \
330             str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
331             str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
332             inventory + "%\n" + \
333             str(world_db["Things"][0]["T_POSY"]) + "\n" + \
334             str(world_db["Things"][0]["T_POSX"]) + "\n" + \
335             str(world_db["MAP_LENGTH"]) + "\n"
336         length = world_db["MAP_LENGTH"]
337         fov = bytearray(b' ' * (length ** 2))
338         ord_v = ord("v")
339         for pos in [pos for pos in range(length ** 2)
340                         if ord_v == world_db["Things"][0]["fovmap"][pos]]:
341             fov[pos] = world_db["MAP"][pos]
342         length = world_db["MAP_LENGTH"]
343         for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
344                       for id in world_db["Things"]
345                       if not world_db["Things"][id]["carried"]
346                       if world_db["Things"][id]["T_TYPE"] == tid
347                       if world_db["Things"][0]["fovmap"][
348                            world_db["Things"][id]["T_POSY"] * length
349                            + world_db["Things"][id]["T_POSX"]] == ord_v]:
350             type = world_db["Things"][id]["T_TYPE"]
351             c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
352             fov[world_db["Things"][id]["T_POSY"] * length
353                 + world_db["Things"][id]["T_POSX"]] = c
354         string = write_map(string, fov)
355         mem = world_db["Things"][0]["T_MEMMAP"][:]
356         for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
357                       for mt in world_db["Things"][0]["T_MEMTHING"]
358                       if mt[0] == tid]:
359              c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
360              mem[(mt[1] * length) + mt[2]] = ord(c)
361         string = write_map(string, mem)
362         atomic_write(io_db["path_worldstate"], string, delete=False)
363         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
364         io_db["worldstate_updateable"] = False
365
366
367 def replay_game():
368     """Replay game from record file.
369
370     Use opts.replay as breakpoint turn to which to replay automatically before
371     switching to manual input by non-meta commands in server input file
372     triggering further reads of record file. Ensure opts.replay is at least 1.
373     Run try_worldstate_update() before each interactive obey()/read_command().
374     """
375     if opts.replay < 1:
376         opts.replay = 1
377     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
378           " (if so late a turn is to be found).")
379     if not os.access(io_db["path_record"], os.F_OK):
380         raise SystemExit("No record file found to replay.")
381     io_db["file_record"] = open(io_db["path_record"], "r")
382     io_db["file_record"].prefix = "record file line "
383     io_db["file_record"].line_n = 1
384     while world_db["TURN"] < opts.replay:
385         line = io_db["file_record"].readline()
386         if "" == line:
387             break
388         obey(line.rstrip(), io_db["file_record"].prefix
389              + str(io_db["file_record"].line_n))
390         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
391     while True:
392         try_worldstate_update()
393         obey(read_command(), "in file", replay=True)
394
395
396 def play_game():
397     """Play game by server input file commands. Before, load save file found.
398
399     If no save file is found, a new world is generated from the commands in the
400     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
401     command and all that follow via the server input file. Run
402     try_worldstate_update() before each interactive obey()/read_command().
403     """
404     if os.access(io_db["path_save"], os.F_OK):
405         obey_lines_in_file(io_db["path_save"], "save")
406     else:
407         if not os.access(io_db["path_worldconf"], os.F_OK):
408             msg = "No world config file from which to start a new world."
409             raise SystemExit(msg)
410         obey_lines_in_file(io_db["path_worldconf"], "world config ",
411                            do_record=True)
412         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
413     while True:
414         try_worldstate_update()
415         obey(read_command(), "in file", do_record=True)
416
417
418 def make_map():
419     """(Re-)make island map.
420
421     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
422     start with one land cell in the middle, then go into cycle of repeatedly
423     selecting a random sea cell and transforming it into land if it is neighbor
424     to land. The cycle ends when a land cell is due to be created at the map's
425     border. Then put some trees on the map (TODO: more precise algorithm desc).
426     """
427
428     def is_neighbor(coordinates, type):
429         y = coordinates[0]
430         x = coordinates[1]
431         length = world_db["MAP_LENGTH"]
432         ind = y % 2
433         diag_west = x + (ind > 0)
434         diag_east = x + (ind < (length - 1))
435         pos = (y * length) + x
436         if (y > 0 and diag_east
437             and type == chr(world_db["MAP"][pos - length + ind])) \
438            or (x < (length - 1)
439                and type == chr(world_db["MAP"][pos + 1])) \
440            or (y < (length - 1) and diag_east
441                and type == chr(world_db["MAP"][pos + length + ind])) \
442            or (y > 0 and diag_west
443                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
444            or (x > 0
445                and type == chr(world_db["MAP"][pos - 1])) \
446            or (y < (length - 1) and diag_west
447                and type == chr(world_db["MAP"][pos + length - (not ind)])):
448             return True
449         return False
450
451     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
452     length = world_db["MAP_LENGTH"]
453     add_half_width = (not (length % 2)) * int(length / 2)
454     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
455     while (1):
456         y = rand.next() % length
457         x = rand.next() % length
458         pos = (y * length) + x
459         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
460             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
461                 break
462             world_db["MAP"][pos] = ord(".")
463     n_trees = int((length ** 2) / 16)
464     i_trees = 0
465     while (i_trees <= n_trees):
466         single_allowed = rand.next() % 32
467         y = rand.next() % length
468         x = rand.next() % length
469         pos = (y * length) + x
470         if "." == chr(world_db["MAP"][pos]) \
471                 and ((not single_allowed) or is_neighbor((y, x), "X")):
472             world_db["MAP"][pos] = ord("X")
473             i_trees += 1
474     # This all-too-precise replica of the original C code misses iter_limit().
475
476
477 def eat_vs_hunger_threshold(thingtype):
478     """Return satiation cost of eating for type. Good food for it must be >."""
479     hunger_unit = hunger_per_turn(thingtype)
480     actiontype = [id for id in world_db["ThingActions"]
481                if world_db["ThingActions"][id]["TA_NAME"] == "use"][0]
482     return world_db["ThingActions"][actiontype]["TA_EFFORT"] * hunger_unit
483
484
485 def update_map_memory(t, age_map=True):
486     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
487
488     def age_some_memdepthmap_on_nonfov_cells():
489         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
490         # ord_v = ord("v")
491         # ord_0 = ord("0")
492         # ord_9 = ord("9")
493         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
494         #             if not ord_v == t["fovmap"][pos]
495         #             if ord_0 <= t["T_MEMDEPTHMAP"][pos]
496         #             if ord_9 > t["T_MEMDEPTHMAP"][pos]
497         #             if not rand.next() % (2 **
498         #                                   (t["T_MEMDEPTHMAP"][pos] - 48))]:
499         #     t["T_MEMDEPTHMAP"][pos] += 1
500         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
501         fovmap = c_pointer_to_bytearray(t["fovmap"])
502         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
503
504     if not t["T_MEMMAP"]:
505         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
506     if not t["T_MEMDEPTHMAP"]:
507         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
508     ord_v = ord("v")
509     ord_0 = ord("0")
510     for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
511                 if ord_v == t["fovmap"][pos]]:
512         t["T_MEMDEPTHMAP"][pos] = ord_0
513         t["T_MEMMAP"][pos] = world_db["MAP"][pos]
514     if age_map:
515         age_some_memdepthmap_on_nonfov_cells()
516     t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
517                        if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
518                                                + mt[2]]]
519     for id in [id for id in world_db["Things"]
520                if not world_db["Things"][id]["carried"]]:
521         type = world_db["Things"][id]["T_TYPE"]
522         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
523             y = world_db["Things"][id]["T_POSY"]
524             x = world_db["Things"][id]["T_POSX"]
525             if ord_v == t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]:
526                 t["T_MEMTHING"].append((type, y, x))
527
528
529 def set_world_inactive():
530     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
531     server_test()
532     if os.access(io_db["path_worldstate"], os.F_OK):
533         os.remove(io_db["path_worldstate"])
534     world_db["WORLD_ACTIVE"] = 0
535
536
537 def integer_test(val_string, min, max=None):
538     """Return val_string if integer >= min & (if max set) <= max, else None."""
539     try:
540         val = int(val_string)
541         if val < min or (max is not None and val > max):
542             raise ValueError
543         return val
544     except ValueError:
545         msg = "Ignoring: Please use integer >= " + str(min)
546         if max is not None:
547             msg += " and <= " + str(max)
548         msg += "."
549         print(msg)
550         return None
551
552
553 def setter(category, key, min, max=None):
554     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
555     if category is None:
556         def f(val_string):
557             val = integer_test(val_string, min, max)
558             if None != val:
559                 world_db[key] = val
560     else:
561         if category == "Thing":
562             id_store = command_tid
563             decorator = test_Thing_id
564         elif category == "ThingType":
565             id_store = command_ttid
566             decorator = test_ThingType_id
567         elif category == "ThingAction":
568             id_store = command_taid
569             decorator = test_ThingAction_id
570
571         @decorator
572         def f(val_string):
573             val = integer_test(val_string, min, max)
574             if None != val:
575                 world_db[category + "s"][id_store.id][key] = val
576     return f
577
578
579 def build_fov_map(t):
580     """Build Thing's FOV map."""
581     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
582     fovmap = c_pointer_to_bytearray(t["fovmap"])
583     map = c_pointer_to_bytearray(world_db["MAP"])
584     if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
585         raise RuntimeError("Malloc error in build_fov_Map().")
586
587
588 def log_help():
589     """Send quick usage info to log."""
590     log("LOG See README file for help.")
591
592
593 def decrement_lifepoints(t):
594     """Decrement t's lifepoints by 1, and if to zero, corpse it.
595
596     If t is the player avatar, only blank its fovmap, so that the client may
597     still display memory data. On non-player things, erase fovmap and memory.
598     Dying actors drop all their things.
599     """
600     t["T_LIFEPOINTS"] -= 1
601     if 0 == t["T_LIFEPOINTS"]:
602         for id in t["T_CARRIES"]:
603             t["T_CARRIES"].remove(id)
604             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
605             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
606             world_db["Things"][id]["carried"] = False
607         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
608         if world_db["Things"][0] == t:
609             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
610             log("You die.")
611             log("See README on how to start over.")
612         else:
613             t["fovmap"] = False
614             t["T_MEMMAP"] = False
615             t["T_MEMDEPTHMAP"] = False
616             t["T_MEMTHING"] = []
617
618
619 def mv_yx_in_dir_legal(dir, y, x):
620     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
621     dir_c = dir.encode("ascii")[0]
622     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
623     if -1 == test:
624         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
625     return (test, libpr.result_y(), libpr.result_x())
626
627
628 def actor_wait(t):
629     """Make t do nothing (but loudly, if player avatar)."""
630     if t == world_db["Things"][0]:
631         log("You wait")
632
633
634 def actor_move(t):
635     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
636     passable = False
637     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
638                                      t["T_POSY"], t["T_POSX"])
639     if 1 == move_result[0]:
640         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
641         hitted = [id for id in world_db["Things"]
642                   if world_db["Things"][id] != t
643                   if world_db["Things"][id]["T_LIFEPOINTS"]
644                   if world_db["Things"][id]["T_POSY"] == move_result[1]
645                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
646         if len(hitted):
647             hit_id = hitted[0]
648             if t == world_db["Things"][0]:
649                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
650                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
651                 log("You wound " + hitted_name + ".")
652             elif 0 == hit_id:
653                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
654                 log(hitter_name +" wounds you.")
655             decrement_lifepoints(world_db["Things"][hit_id])
656             return
657         passable = "." == chr(world_db["MAP"][pos])
658     dir = [dir for dir in directions_db
659            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
660     if passable:
661         t["T_POSY"] = move_result[1]
662         t["T_POSX"] = move_result[2]
663         for id in t["T_CARRIES"]:
664             world_db["Things"][id]["T_POSY"] = move_result[1]
665             world_db["Things"][id]["T_POSX"] = move_result[2]
666         build_fov_map(t)
667         if t == world_db["Things"][0]:
668             log("You move " + dir + ".")
669     elif t == world_db["Things"][0]:
670         log("You fail to move " + dir + ".")
671
672
673 def actor_pick_up(t):
674     """Make t pick up (topmost?) Thing from ground into inventory.
675
676     Define topmostness by how low the thing's type ID is.
677     """
678     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
679            if not world_db["Things"][id]["carried"]
680            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
681            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
682     if len(ids):
683         lowest_tid = -1
684         for iid in ids:
685             tid = world_db["Things"][iid]["T_TYPE"]
686             if lowest_tid == -1 or tid < lowest_tid:
687                 id = iid
688                 lowest_tid = tid
689         world_db["Things"][id]["carried"] = True
690         t["T_CARRIES"].append(id)
691         if t == world_db["Things"][0]:
692                 log("You pick up an object.")
693     elif t == world_db["Things"][0]:
694             log("You try to pick up an object, but there is none.")
695
696
697 def actor_drop(t):
698     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
699     # TODO: Handle case where T_ARGUMENT matches nothing.
700     if len(t["T_CARRIES"]):
701         id = t["T_CARRIES"][t["T_ARGUMENT"]]
702         t["T_CARRIES"].remove(id)
703         world_db["Things"][id]["carried"] = False
704         if t == world_db["Things"][0]:
705             log("You drop an object.")
706     elif t == world_db["Things"][0]:
707        log("You try to drop an object, but you own none.")
708
709
710 def actor_use(t):
711     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
712     # TODO: Handle case where T_ARGUMENT matches nothing.
713     if len(t["T_CARRIES"]):
714         id = t["T_CARRIES"][t["T_ARGUMENT"]]
715         type = world_db["Things"][id]["T_TYPE"]
716         if world_db["ThingTypes"][type]["TT_TOOL"] == "food":
717             t["T_CARRIES"].remove(id)
718             del world_db["Things"][id]
719             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_TOOLPOWER"]
720             if t == world_db["Things"][0]:
721                 log("You consume this object.")
722         elif t == world_db["Things"][0]:
723             log("You try to use this object, but fail.")
724     elif t == world_db["Things"][0]:
725         log("You try to use an object, but you own none.")
726
727
728 def thingproliferation(t, prol_map):
729     """To chance of 1/TT_PROLIFERATE, create t offspring in open neighbor cell.
730
731     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
732     marked '.' in prol_map. If there are several map cell candidates, one is
733     selected randomly.
734     """
735     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
736     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
737         candidates = []
738         for dir in [directions_db[key] for key in sorted(directions_db.keys())]:
739             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
740             if mv_result[0] and  ord('.') == prol_map[mv_result[1]
741                                                       * world_db["MAP_LENGTH"]
742                                                       + mv_result[2]]:
743                 candidates.append((mv_result[1], mv_result[2]))
744         if len(candidates):
745             i = rand.next() % len(candidates)
746             id = id_setter(-1, "Things")
747             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
748             world_db["Things"][id] = newT
749
750
751 def try_healing(t):
752     """If t's HP < max, increment them if well-nourished, maybe waiting."""
753     if t["T_LIFEPOINTS"] < \
754        world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
755         wait_id = [id for id in world_db["ThingActions"]
756                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
757         wait_divider = 8 if t["T_COMMAND"] == wait_id else 1
758         testval = int(abs(t["T_SATIATION"]) / wait_divider)
759         if (testval <= 1 or 1 == (rand.next() % testval)):
760             t["T_LIFEPOINTS"] += 1
761             if t == world_db["Things"][0]:
762                 log("You heal.")
763
764
765 def hunger_per_turn(type_id):
766     """The amount of satiation score lost per turn for things of given type."""
767     return int(math.sqrt(world_db["ThingTypes"][type_id]["TT_LIFEPOINTS"]))
768
769
770 def hunger(t):
771     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
772     if t["T_SATIATION"] > -32768:
773         t["T_SATIATION"] -= hunger_per_turn(t["T_TYPE"])
774     if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
775         if t == world_db["Things"][0]:
776             if t["T_SATIATION"] < 0:
777                 log("You suffer from hunger.")
778             else:
779                 log("You suffer from over-eating.")
780         decrement_lifepoints(t)
781
782
783 def get_dir_to_target(t, filter):
784     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
785
786     The path-wise nearest target is chosen, via the shortest available path.
787     Target must not be t. On succcess, return positive value, else False.
788     Filters:
789     "a": Thing in FOV is below a certain distance, animate, but of ThingType
790          that is not t's, and starts out weaker than t is; build path as
791          avoiding things of t's ThingType
792     "f": neighbor cell (not inhabited by any animate Thing) further away from
793          animate Thing not further than x steps away and in FOV and of a
794          ThingType that is not t's, and starts out stronger or as strong as t
795          is currently; or (cornered), if no such flight cell, but Thing of
796          above criteria is too near,1 a cell closer to it, or, if less near,
797          just wait
798     "c": Thing in memorized map is consumable
799     "s": memory map cell with greatest-reachable degree of unexploredness
800     """
801
802     def zero_score_map_where_char_on_memdepthmap(c):
803         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
804         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
805         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
806         #     set_map_score(i, 0)
807         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
808         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
809             raise RuntimeError("No score map allocated for "
810                                "zero_score_map_where_char_on_memdepthmap().")
811
812     def set_map_score(pos, score):
813         test = libpr.set_map_score(pos, score)
814         if test:
815             raise RuntimeError("No score map allocated for set_map_score().")
816
817     def get_map_score(pos):
818         result = libpr.get_map_score(pos)
819         if result < 0:
820             raise RuntimeError("No score map allocated for get_map_score().")
821         return result
822
823     def seeing_thing():
824         if t["fovmap"] and ("a" == filter or "f" == filter):
825             for id in world_db["Things"]:
826                 Thing = world_db["Things"][id]
827                 if Thing != t and Thing["T_LIFEPOINTS"] and \
828                    t["T_TYPE"] != Thing["T_TYPE"] and \
829                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
830                                           * world_db["MAP_LENGTH"])
831                                           + Thing["T_POSX"]]):
832                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
833                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
834                         t["T_LIFEPOINTS"]) \
835                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
836                             t["T_LIFEPOINTS"]):
837                         return True
838         elif t["T_MEMMAP"] and "c" == filter:
839             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
840             for mt in t["T_MEMTHING"]:
841                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
842                                             + mt[2]]) \
843                    and world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food" \
844                    and world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"] \
845                        > eat_cost:
846                     return True
847         return False
848
849     def set_cells_passable_on_memmap_to_65534_on_scoremap():
850         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
851         # ord_dot = ord(".")
852         # memmap = t["T_MEMMAP"]
853         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
854         #            if ord_dot == memmap[i]]:
855         #     set_map_score(i, 65534) # i.e. 65535-1
856         map = c_pointer_to_bytearray(t["T_MEMMAP"])
857         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
858             raise RuntimeError("No score map allocated for set_cells_passable"
859                                "_on_memmap_to_65534_on_scoremap().")
860
861     def init_score_map():
862         test = libpr.init_score_map()
863         if test:
864             raise RuntimeError("Malloc error in init_score_map().")
865         ord_v = ord("v")
866         ord_blank = ord(" ")
867         set_cells_passable_on_memmap_to_65534_on_scoremap()
868         if "a" == filter:
869             for id in world_db["Things"]:
870                 Thing = world_db["Things"][id]
871                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
872                     + Thing["T_POSX"]
873                 if t != Thing and Thing["T_LIFEPOINTS"] and \
874                    t["T_TYPE"] != Thing["T_TYPE"] and \
875                    ord_v == t["fovmap"][pos] and \
876                    t["T_LIFEPOINTS"] > \
877                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
878                     set_map_score(pos, 0)
879                 elif t["T_TYPE"] == Thing["T_TYPE"]:
880                     set_map_score(pos, 65535)
881         elif "f" == filter:
882             for id in [id for id in world_db["Things"]
883                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
884                 Thing = world_db["Things"][id]
885                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
886                     + Thing["T_POSX"]
887                 if t["T_TYPE"] != Thing["T_TYPE"] and \
888                    ord_v == t["fovmap"][pos] and \
889                    t["T_LIFEPOINTS"] <= \
890                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
891                     set_map_score(pos, 0)
892         elif "c" == filter:
893             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
894             for mt in [mt for mt in t["T_MEMTHING"]
895                        if ord_blank != t["T_MEMMAP"][mt[1]
896                                                      * world_db["MAP_LENGTH"]
897                                                      + mt[2]]
898                        if world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"
899                        if world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"]
900                            > eat_cost]:
901                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
902         elif "s" == filter:
903             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
904
905     def rand_target_dir(neighbors, cmp, dirs):
906         candidates = []
907         n_candidates = 0
908         for i in range(len(dirs)):
909             if cmp == neighbors[i]:
910                 candidates.append(dirs[i])
911                 n_candidates += 1
912         return candidates[rand.next() % n_candidates] if n_candidates else 0
913
914     def get_neighbor_scores(dirs, eye_pos):
915         scores = []
916         if libpr.ready_neighbor_scores(eye_pos):
917             raise RuntimeError("No score map allocated for " +
918                                "ready_neighbor_scores.()")
919         for i in range(len(dirs)):
920             scores.append(libpr.get_neighbor_score(i))
921         return scores
922
923     def get_dir_from_neighbors():
924         dir_to_target = False
925         dirs = "edcxsw"
926         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
927         neighbors = get_neighbor_scores(dirs, eye_pos)
928         if "f" == filter:
929             inhabited = [world_db["Things"][id]["T_POSY"]
930                          * world_db["MAP_LENGTH"]
931                          + world_db["Things"][id]["T_POSX"]
932                          for id in world_db["Things"]
933                          if world_db["Things"][id]["T_LIFEPOINTS"]]
934             for i in range(len(dirs)):
935                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
936                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
937                     + libpr.result_x()
938                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
939                     neighbors[i] = 65535
940                     break
941         minmax_start = 0 if "f" == filter else 65535 - 1
942         minmax_neighbor = minmax_start
943         for i in range(len(dirs)):
944             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
945                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
946                or ("f" != filter and minmax_neighbor > neighbors[i]):
947                 minmax_neighbor = neighbors[i]
948         if minmax_neighbor != minmax_start:
949             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
950         if "f" == filter:
951             if not dir_to_target:
952                 if 1 == get_map_score(eye_pos):
953                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
954                 elif 3 >= get_map_score(eye_pos):
955                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
956                                       if
957                                       world_db["ThingActions"][id]["TA_NAME"]
958                                       == "wait"][0]
959                     return 1
960             elif dir_to_target and 3 < get_map_score(eye_pos):
961                 dir_to_target = 0
962         elif "a" == filter and 10 <= get_map_score(eye_pos):
963             dir_to_target = 0
964         return dir_to_target
965
966     dir_to_target = False
967     mem_depth_c = b' '
968     run_i = 9 + 1 if "s" == filter else 1
969     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
970         run_i -= 1
971         init_score_map()
972         mem_depth_c = b'9' if b' ' == mem_depth_c \
973             else bytes([mem_depth_c[0] - 1])
974         if libpr.dijkstra_map():
975             raise RuntimeError("No score map allocated for dijkstra_map().")
976         dir_to_target = get_dir_from_neighbors()
977         libpr.free_score_map()
978         if dir_to_target and str == type(dir_to_target):
979             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
980                               if world_db["ThingActions"][id]["TA_NAME"]
981                               == "move"][0]
982             t["T_ARGUMENT"] = ord(dir_to_target)
983     return dir_to_target
984
985
986 def standing_on_food(t):
987     """Return True/False whether t is standing on healthy consumable."""
988     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
989     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
990                if not world_db["Things"][id]["carried"]
991                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
992                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
993                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
994                   ["TT_TOOL"] == "food"
995                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
996                   ["TT_TOOLPOWER"] > eat_cost]:
997         return True
998     return False
999
1000
1001 def get_inventory_slot_to_consume(t):
1002     """Return invent. slot of healthiest consumable(if any healthy),else -1."""
1003     cmp_food = -1
1004     selection = -1
1005     i = 0
1006     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1007     for id in t["T_CARRIES"]:
1008         type = world_db["Things"][id]["T_TYPE"]
1009         if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
1010            and world_db["ThingTypes"][type]["TT_TOOLPOWER"]:
1011             nutvalue = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1012             tmp_cmp = abs(t["T_SATIATION"] + nutvalue - eat_cost)
1013             if (cmp_food < 0 and tmp_cmp < abs(t["T_SATIATION"])) \
1014             or tmp_cmp < cmp_food:
1015                 cmp_food = tmp_cmp
1016                 selection = i
1017         i += 1
1018     return selection
1019
1020
1021 def ai(t):
1022     """Determine next command/argment for actor t via AI algorithms.
1023
1024     AI will look for, and move towards, enemies (animate Things not of their
1025     own ThingType); if they see none, they will consume consumables in their
1026     inventory; if there are none, they will pick up what they stand on if they
1027     stand on consumables; if they stand on none, they will move towards the
1028     next consumable they see or remember on the map; if they see or remember
1029     none, they will explore parts of the map unseen since ever or for at least
1030     one turn; if there is nothing to explore, they will simply wait.
1031     """
1032     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1033                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1034     if not get_dir_to_target(t, "f"):
1035         sel = get_inventory_slot_to_consume(t)
1036         if -1 != sel:
1037             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1038                               if world_db["ThingActions"][id]["TA_NAME"]
1039                               == "use"][0]
1040             t["T_ARGUMENT"] = sel
1041         elif standing_on_food(t):
1042             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1043                               if world_db["ThingActions"][id]["TA_NAME"]
1044                               == "pick_up"][0]
1045         elif (not get_dir_to_target(t, "c")) and \
1046              (not get_dir_to_target(t, "a")):
1047             get_dir_to_target(t, "s")
1048
1049
1050 def turn_over():
1051     """Run game world and its inhabitants until new player input expected."""
1052     id = 0
1053     whilebreaker = False
1054     while world_db["Things"][0]["T_LIFEPOINTS"]:
1055         proliferable_map = world_db["MAP"][:]
1056         for id in [id for id in world_db["Things"]
1057                    if not world_db["Things"][id]["carried"]]:
1058             y = world_db["Things"][id]["T_POSY"]
1059             x = world_db["Things"][id]["T_POSX"]
1060             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1061         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1062             if not id in world_db["Things"] or \
1063                world_db["Things"][id]["carried"]:   # May have been consumed or
1064                 continue                            # picked up during turn …
1065             Thing = world_db["Things"][id]
1066             if Thing["T_LIFEPOINTS"]:
1067                 if not Thing["T_COMMAND"]:
1068                     update_map_memory(Thing)
1069                     if 0 == id:
1070                         whilebreaker = True
1071                         break
1072                     ai(Thing)
1073                 try_healing(Thing)
1074                 hunger(Thing)
1075                 if Thing["T_LIFEPOINTS"]:
1076                     Thing["T_PROGRESS"] += 1
1077                     taid = [a for a in world_db["ThingActions"]
1078                               if a == Thing["T_COMMAND"]][0]
1079                     ThingAction = world_db["ThingActions"][taid]
1080                     if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1081                         eval("actor_" + ThingAction["TA_NAME"])(Thing)
1082                         Thing["T_COMMAND"] = 0
1083                         Thing["T_PROGRESS"] = 0
1084             thingproliferation(Thing, proliferable_map)
1085         if whilebreaker:
1086             break
1087         world_db["TURN"] += 1
1088
1089
1090 def new_Thing(type, pos=(0, 0)):
1091     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1092     thing = {
1093         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1094         "T_ARGUMENT": 0,
1095         "T_PROGRESS": 0,
1096         "T_SATIATION": 0,
1097         "T_COMMAND": 0,
1098         "T_TYPE": type,
1099         "T_POSY": pos[0],
1100         "T_POSX": pos[1],
1101         "T_CARRIES": [],
1102         "carried": False,
1103         "T_MEMTHING": [],
1104         "T_MEMMAP": False,
1105         "T_MEMDEPTHMAP": False,
1106         "fovmap": False
1107     }
1108     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1109         build_fov_map(thing)
1110     return thing
1111
1112
1113 def id_setter(id, category, id_store=False, start_at_1=False):
1114     """Set ID of object of category to manipulate ID unused? Create new one.
1115     The ID is stored as id_store.id (if id_store is set). If the integer of the
1116     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1117     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1118     always returned when no new object is created, else the new object's ID.
1119     """
1120     min = 0 if start_at_1 else -1
1121     if str == type(id):
1122         id = integer_test(id, min)
1123     if None != id:
1124         if id in world_db[category]:
1125             if id_store:
1126                 id_store.id = id
1127             return None
1128         else:
1129             if (start_at_1 and 0 == id) \
1130                or ((not start_at_1) and (id < 0)):
1131                 id = 0 if start_at_1 else -1
1132                 while 1:
1133                     id = id + 1
1134                     if id not in world_db[category]:
1135                         break
1136             if id_store:
1137                 id_store.id = id
1138     return id
1139
1140
1141 def command_ping():
1142     """Send PONG line to server output file."""
1143     strong_write(io_db["file_out"], "PONG\n")
1144
1145
1146 def command_quit():
1147     """Abort server process."""
1148     if None == opts.replay:
1149         if world_db["WORLD_ACTIVE"]:
1150             save_world()
1151         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1152     raise SystemExit("received QUIT command")
1153
1154
1155 def command_thingshere(str_y, str_x):
1156     """Write to out file list of Things known to player at coordinate y, x."""
1157     if world_db["WORLD_ACTIVE"]:
1158         y = integer_test(str_y, 0, 255)
1159         x = integer_test(str_x, 0, 255)
1160         length = world_db["MAP_LENGTH"]
1161         if None != y and None != x and y < length and x < length:
1162             pos = (y * world_db["MAP_LENGTH"]) + x
1163             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1164             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1165                 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1166                               for id in world_db["Things"]
1167                               if not world_db["Things"][id]["carried"]
1168                               if world_db["Things"][id]["T_TYPE"] == tid
1169                               if y == world_db["Things"][id]["T_POSY"]
1170                               if x == world_db["Things"][id]["T_POSX"]]:
1171                     type = world_db["Things"][id]["T_TYPE"]
1172                     name = world_db["ThingTypes"][type]["TT_NAME"]
1173                     strong_write(io_db["file_out"], name + "\n")
1174             else:
1175                 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1176                               for mt in world_db["Things"][0]["T_MEMTHING"]
1177                               if mt[0] == tid if y == mt[1] if x == mt[2]]:
1178                     name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1179                     strong_write(io_db["file_out"], name + "\n")
1180             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1181         else:
1182             print("Ignoring: Invalid map coordinates.")
1183     else:
1184         print("Ignoring: Command only works on existing worlds.")
1185
1186
1187 def play_commander(action, args=False):
1188     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1189
1190     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1191     """
1192
1193     def set_command():
1194         id = [x for x in world_db["ThingActions"]
1195               if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1196         world_db["Things"][0]["T_COMMAND"] = id
1197         turn_over()
1198
1199     def set_command_and_argument_int(str_arg):
1200         val = integer_test(str_arg, 0, 255)
1201         if None != val:
1202             world_db["Things"][0]["T_ARGUMENT"] = val
1203             set_command()
1204
1205     def set_command_and_argument_movestring(str_arg):
1206         if str_arg in directions_db:
1207             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1208             set_command()
1209         else:
1210             print("Ignoring: Argument must be valid direction string.")
1211
1212     if action == "move":
1213         return set_command_and_argument_movestring
1214     elif args:
1215         return set_command_and_argument_int
1216     else:
1217         return set_command
1218
1219
1220 def command_seedrandomness(seed_string):
1221     """Set rand seed to int(seed_string)."""
1222     val = integer_test(seed_string, 0, 4294967295)
1223     if None != val:
1224         rand.seed = val
1225
1226
1227 def command_makeworld(seed_string):
1228     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1229
1230     Seed rand with seed. Do more only with a "wait" ThingAction and
1231     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1232     world_db["Things"] emptied, call make_map() and set
1233     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1234     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1235     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1236     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1237     Call log_help().
1238     """
1239
1240     def free_pos():
1241         i = 0
1242         while 1:
1243             err = "Space to put thing on too hard to find. Map too small?"
1244             while 1:
1245                 y = rand.next() % world_db["MAP_LENGTH"]
1246                 x = rand.next() % world_db["MAP_LENGTH"]
1247                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1248                     break
1249                 i += 1
1250                 if i == 65535:
1251                     raise SystemExit(err)
1252             # Replica of C code, wrongly ignores animatedness of new Thing.
1253             pos_clear = (0 == len([id for id in world_db["Things"]
1254                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1255                                    if world_db["Things"][id]["T_POSY"] == y
1256                                    if world_db["Things"][id]["T_POSX"] == x]))
1257             if pos_clear:
1258                 break
1259         return (y, x)
1260
1261     val = integer_test(seed_string, 0, 4294967295)
1262     if None == val:
1263         return
1264     rand.seed = val
1265     player_will_be_generated = False
1266     playertype = world_db["PLAYER_TYPE"]
1267     for ThingType in world_db["ThingTypes"]:
1268         if playertype == ThingType:
1269             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1270                 player_will_be_generated = True
1271             break
1272     if not player_will_be_generated:
1273         print("Ignoring: No player type with start number >0 defined.")
1274         return
1275     wait_action = False
1276     for ThingAction in world_db["ThingActions"]:
1277         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1278             wait_action = True
1279     if not wait_action:
1280         print("Ignoring beyond SEED_MAP: " +
1281               "No thing action with name 'wait' defined.")
1282         return
1283     world_db["Things"] = {}
1284     make_map()
1285     world_db["WORLD_ACTIVE"] = 1
1286     world_db["TURN"] = 1
1287     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1288         id = id_setter(-1, "Things")
1289         world_db["Things"][id] = new_Thing(playertype, free_pos())
1290     if not world_db["Things"][0]["fovmap"]:
1291         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1292         world_db["Things"][0]["fovmap"] = empty_fovmap
1293     update_map_memory(world_db["Things"][0])
1294     for type in world_db["ThingTypes"]:
1295         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1296             if type != playertype:
1297                 id = id_setter(-1, "Things")
1298                 world_db["Things"][id] = new_Thing(type, free_pos())
1299     strong_write(io_db["file_out"], "NEW_WORLD\n")
1300     log_help()
1301
1302
1303 def command_maplength(maplength_string):
1304     """Redefine map length. Invalidate map, therefore lose all things on it."""
1305     val = integer_test(maplength_string, 1, 256)
1306     if None != val:
1307         world_db["MAP_LENGTH"] = val
1308         world_db["MAP"] = False
1309         set_world_inactive()
1310         world_db["Things"] = {}
1311         libpr.set_maplength(val)
1312
1313
1314 def command_worldactive(worldactive_string):
1315     """Toggle world_db["WORLD_ACTIVE"] if possible.
1316
1317     An active world can always be set inactive. An inactive world can only be
1318     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1319     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1320     Also call log_help().
1321     """
1322     val = integer_test(worldactive_string, 0, 1)
1323     if None != val:
1324         if 0 != world_db["WORLD_ACTIVE"]:
1325             if 0 == val:
1326                 set_world_inactive()
1327             else:
1328                 print("World already active.")
1329         elif 0 == world_db["WORLD_ACTIVE"]:
1330             wait_exists = False
1331             for ThingAction in world_db["ThingActions"]:
1332                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1333                     wait_exists = True
1334                     break
1335             player_exists = False
1336             for Thing in world_db["Things"]:
1337                 if 0 == Thing:
1338                     player_exists = True
1339                     break
1340             if wait_exists and player_exists and world_db["MAP"]:
1341                 for id in world_db["Things"]:
1342                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1343                         build_fov_map(world_db["Things"][id])
1344                         if 0 == id:
1345                             update_map_memory(world_db["Things"][id], False)
1346                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1347                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1348                     world_db["Things"][0]["fovmap"] = empty_fovmap
1349                 world_db["WORLD_ACTIVE"] = 1
1350                 log_help()
1351             else:
1352                 print("Ignoring: Not all conditions for world activation met.")
1353
1354
1355 def test_for_id_maker(object, category):
1356     """Return decorator testing for object having "id" attribute."""
1357     def decorator(f):
1358         def helper(*args):
1359             if hasattr(object, "id"):
1360                 f(*args)
1361             else:
1362                 print("Ignoring: No " + category +
1363                       " defined to manipulate yet.")
1364         return helper
1365     return decorator
1366
1367
1368 def command_tid(id_string):
1369     """Set ID of Thing to manipulate. ID unused? Create new one.
1370
1371     Default new Thing's type to the first available ThingType, others: zero.
1372     """
1373     id = id_setter(id_string, "Things", command_tid)
1374     if None != id:
1375         if world_db["ThingTypes"] == {}:
1376             print("Ignoring: No ThingType to settle new Thing in.")
1377             return
1378         type = list(world_db["ThingTypes"].keys())[0]
1379         world_db["Things"][id] = new_Thing(type)
1380
1381
1382 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1383
1384
1385 @test_Thing_id
1386 def command_tcommand(str_int):
1387     """Set T_COMMAND of selected Thing."""
1388     val = integer_test(str_int, 0)
1389     if None != val:
1390         if 0 == val or val in world_db["ThingActions"]:
1391             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1392         else:
1393             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1394
1395
1396 @test_Thing_id
1397 def command_ttype(str_int):
1398     """Set T_TYPE of selected Thing."""
1399     val = integer_test(str_int, 0)
1400     if None != val:
1401         if val in world_db["ThingTypes"]:
1402             world_db["Things"][command_tid.id]["T_TYPE"] = val
1403         else:
1404             print("Ignoring: ThingType ID belongs to no known ThingType.")
1405
1406
1407 @test_Thing_id
1408 def command_tcarries(str_int):
1409     """Append int(str_int) to T_CARRIES of selected Thing.
1410
1411     The ID int(str_int) must not be of the selected Thing, and must belong to a
1412     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1413     """
1414     val = integer_test(str_int, 0)
1415     if None != val:
1416         if val == command_tid.id:
1417             print("Ignoring: Thing cannot carry itself.")
1418         elif val in world_db["Things"] \
1419                 and not world_db["Things"][val]["carried"]:
1420             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1421             world_db["Things"][val]["carried"] = True
1422         else:
1423             print("Ignoring: Thing not available for carrying.")
1424     # Note that the whole carrying structure is different from the C version:
1425     # Carried-ness is marked by a "carried" flag, not by Things containing
1426     # Things internally.
1427
1428
1429 @test_Thing_id
1430 def command_tmemthing(str_t, str_y, str_x):
1431     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1432
1433     The type must fit to an existing ThingType, and the position into the map.
1434     """
1435     type = integer_test(str_t, 0)
1436     posy = integer_test(str_y, 0, 255)
1437     posx = integer_test(str_x, 0, 255)
1438     if None != type and None != posy and None != posx:
1439         if type not in world_db["ThingTypes"] \
1440            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1441             print("Ignoring: Illegal value for thing type or position.")
1442         else:
1443             memthing = (type, posy, posx)
1444             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1445
1446
1447 def setter_map(maptype):
1448     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1449
1450     If no map of maptype exists yet, initialize it with ' ' bytes first.
1451     """
1452
1453     def valid_map_line(str_int, mapline):
1454         val = integer_test(str_int, 0, 255)
1455         if None != val:
1456             if val >= world_db["MAP_LENGTH"]:
1457                 print("Illegal value for map line number.")
1458             elif len(mapline) != world_db["MAP_LENGTH"]:
1459                 print("Map line length is unequal map width.")
1460             else:
1461                 return val
1462         return None
1463
1464     def nonThingMap_helper(str_int, mapline):
1465         val = valid_map_line(str_int, mapline)
1466         if None != val:
1467             length = world_db["MAP_LENGTH"]
1468             if not world_db["MAP"]:
1469                 map = bytearray(b' ' * (length ** 2))
1470             else:
1471                 map = world_db["MAP"]
1472             map[val * length:(val * length) + length] = mapline.encode()
1473             if not world_db["MAP"]:
1474                 world_db["MAP"] = map
1475
1476     @test_Thing_id
1477     def ThingMap_helper(str_int, mapline):
1478         val = valid_map_line(str_int, mapline)
1479         if None != val:
1480             length = world_db["MAP_LENGTH"]
1481             if not world_db["Things"][command_tid.id][maptype]:
1482                 map = bytearray(b' ' * (length ** 2))
1483             else:
1484                 map = world_db["Things"][command_tid.id][maptype]
1485             map[val * length:(val * length) + length] = mapline.encode()
1486             if not world_db["Things"][command_tid.id][maptype]:
1487                 world_db["Things"][command_tid.id][maptype] = map
1488
1489     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1490
1491
1492
1493 def setter_tpos(axis):
1494     """Generate setter for T_POSX or  T_POSY of selected Thing.
1495
1496     If world is active, rebuilds animate things' fovmap, player's memory map.
1497     """
1498     @test_Thing_id
1499     def helper(str_int):
1500         val = integer_test(str_int, 0, 255)
1501         if None != val:
1502             if val < world_db["MAP_LENGTH"]:
1503                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1504                 if world_db["WORLD_ACTIVE"] \
1505                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1506                     build_fov_map(world_db["Things"][command_tid.id])
1507                     if 0 == command_tid.id:
1508                         update_map_memory(world_db["Things"][command_tid.id])
1509             else:
1510                 print("Ignoring: Position is outside of map.")
1511     return helper
1512
1513
1514 def command_ttid(id_string):
1515     """Set ID of ThingType to manipulate. ID unused? Create new one.
1516
1517     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
1518     "", others: 0. 
1519     """
1520     id = id_setter(id_string, "ThingTypes", command_ttid)
1521     if None != id:
1522         world_db["ThingTypes"][id] = {
1523             "TT_NAME": "(none)",
1524             "TT_TOOLPOWER": 0,
1525             "TT_LIFEPOINTS": 0,
1526             "TT_PROLIFERATE": 0,
1527             "TT_START_NUMBER": 0,
1528             "TT_SYMBOL": "?",
1529             "TT_CORPSE_ID": id,
1530             "TT_TOOL": ""
1531         }
1532
1533
1534 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1535
1536
1537 @test_ThingType_id
1538 def command_ttname(name):
1539     """Set TT_NAME of selected ThingType."""
1540     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1541
1542
1543 @test_ThingType_id
1544 def command_tttool(name):
1545     """Set TT_TOOL of selected ThingType."""
1546     world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
1547
1548
1549 @test_ThingType_id
1550 def command_ttsymbol(char):
1551     """Set TT_SYMBOL of selected ThingType. """
1552     if 1 == len(char):
1553         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1554     else:
1555         print("Ignoring: Argument must be single character.")
1556
1557
1558 @test_ThingType_id
1559 def command_ttcorpseid(str_int):
1560     """Set TT_CORPSE_ID of selected ThingType."""
1561     val = integer_test(str_int, 0)
1562     if None != val:
1563         if val in world_db["ThingTypes"]:
1564             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1565         else:
1566             print("Ignoring: Corpse ID belongs to no known ThignType.")
1567
1568
1569 def command_taid(id_string):
1570     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1571
1572     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1573     """
1574     id = id_setter(id_string, "ThingActions", command_taid, True)
1575     if None != id:
1576         world_db["ThingActions"][id] = {
1577             "TA_EFFORT": 1,
1578             "TA_NAME": "wait"
1579         }
1580
1581
1582 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1583
1584
1585 @test_ThingAction_id
1586 def command_taname(name):
1587     """Set TA_NAME of selected ThingAction.
1588
1589     The name must match a valid thing action function. If after the name
1590     setting no ThingAction with name "wait" remains, call set_world_inactive().
1591     """
1592     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1593        or name == "pick_up":
1594         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1595         if 1 == world_db["WORLD_ACTIVE"]:
1596             wait_defined = False
1597             for id in world_db["ThingActions"]:
1598                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1599                     wait_defined = True
1600                     break
1601             if not wait_defined:
1602                 set_world_inactive()
1603     else:
1604         print("Ignoring: Invalid action name.")
1605     # In contrast to the original,naming won't map a function to a ThingAction.
1606
1607
1608 def command_ai():
1609     """Call ai() on player Thing, then turn_over()."""
1610     ai(world_db["Things"][0])
1611     turn_over()
1612
1613
1614 """Commands database.
1615
1616 Map command start tokens to ([0]) number of expected command arguments, ([1])
1617 the command's meta-ness (i.e. is it to be written to the record file, is it to
1618 be ignored in replay mode if read from server input file), and ([2]) a function
1619 to be called on it.
1620 """
1621 commands_db = {
1622     "QUIT": (0, True, command_quit),
1623     "PING": (0, True, command_ping),
1624     "THINGS_HERE": (2, True, command_thingshere),
1625     "MAKE_WORLD": (1, False, command_makeworld),
1626     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1627     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1628     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1629     "MAP_LENGTH": (1, False, command_maplength),
1630     "WORLD_ACTIVE": (1, False, command_worldactive),
1631     "MAP": (2, False, setter_map("MAP")),
1632     "TA_ID": (1, False, command_taid),
1633     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1634     "TA_NAME": (1, False, command_taname),
1635     "TT_ID": (1, False, command_ttid),
1636     "TT_NAME": (1, False, command_ttname),
1637     "TT_TOOL": (1, False, command_tttool),
1638     "TT_SYMBOL": (1, False, command_ttsymbol),
1639     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1640     "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
1641     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1642                                          0, 255)),
1643     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1644                                         0, 65535)),
1645     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1646     "T_ID": (1, False, command_tid),
1647     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1648     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1649     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1650     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1651     "T_COMMAND": (1, False, command_tcommand),
1652     "T_TYPE": (1, False, command_ttype),
1653     "T_CARRIES": (1, False, command_tcarries),
1654     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1655     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1656     "T_MEMTHING": (3, False, command_tmemthing),
1657     "T_POSY": (1, False, setter_tpos("Y")),
1658     "T_POSX": (1, False, setter_tpos("X")),
1659     "wait": (0, False, play_commander("wait")),
1660     "move": (1, False, play_commander("move")),
1661     "pick_up": (0, False, play_commander("pick_up")),
1662     "drop": (1, False, play_commander("drop", True)),
1663     "use": (1, False, play_commander("use", True)),
1664     "ai": (0, False, command_ai)
1665 }
1666 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1667
1668
1669 """World state database. With sane default values. (Randomness is in rand.)"""
1670 world_db = {
1671     "TURN": 0,
1672     "MAP_LENGTH": 64,
1673     "PLAYER_TYPE": 0,
1674     "WORLD_ACTIVE": 0,
1675     "MAP": False,
1676     "ThingActions": {},
1677     "ThingTypes": {},
1678     "Things": {}
1679 }
1680
1681 """Mapping of direction names to internal direction chars."""
1682 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1683                  "west": "s", "north-west": "w", "north-east": "e"}
1684
1685 """File IO database."""
1686 io_db = {
1687     "path_save": "save",
1688     "path_record": "record_save",
1689     "path_worldconf": "confserver/world",
1690     "path_server": "server/",
1691     "path_in": "server/in",
1692     "path_out": "server/out",
1693     "path_worldstate": "server/worldstate",
1694     "tmp_suffix": "_tmp",
1695     "kicked_by_rival": False,
1696     "worldstate_updateable": False
1697 }
1698
1699
1700 try:
1701     libpr = prep_library()
1702     rand = RandomnessIO()
1703     opts = parse_command_line_arguments()
1704     if opts.savefile:
1705         io_db["path_save"] = opts.savefile
1706         io_db["path_record"] = "record_" + opts.savefile
1707     setup_server_io()
1708     if opts.verbose:
1709         io_db["verbose"] = True
1710     if None != opts.replay:
1711         replay_game()
1712     else:
1713         play_game()
1714 except SystemExit as exit:
1715     print("ABORTING: " + exit.args[0])
1716 except:
1717     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1718     raise
1719 finally:
1720     cleanup_server_io()