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