home · contact · privacy
Let dying actors drop their things.
[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         for id in t["T_CARRIES"]:
593             t["T_CARRIES"].remove(id)
594             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
595             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
596         sadness = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
597         world_db["GOD_MOOD"] -= sadness  # #        
598         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
599         if world_db["Things"][0] == t:
600             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
601             strong_write(io_db["file_out"], "LOG You die.\n")
602         else:
603             t["fovmap"] = False
604             t["T_MEMMAP"] = False
605             t["T_MEMDEPTHMAP"] = False
606             t["T_MEMTHING"] = []
607         return sadness  # #
608     return 0  # #
609
610
611 def add_gods_favor(i): # #
612     """"Add to GOD_FAVOR, multiplied with factor growing log. with GOD_MOOD."""
613     def favor_multiplier(i):
614         x = 100
615         threshold = math.e * x
616         mood = world_db["GOD_MOOD"]
617         if i > 0:
618             if mood > threshold:
619                 i = i * math.log(mood / x)
620             elif -mood > threshold:
621                 i = i / math.log(-mood / x)
622         elif i < 0:
623             if -mood > threshold:
624                 i = i * math.log(-mood / x)
625             if mood > threshold:
626                 i = i / math.log(mood / x)
627         return int(i)
628     world_db["GOD_FAVOR"] += favor_multiplier(i)
629
630
631 def mv_yx_in_dir_legal(dir, y, x):
632     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
633     dir_c = dir.encode("ascii")[0]
634     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
635     if -1 == test:
636         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
637     return (test, libpr.result_y(), libpr.result_x())
638
639
640 def actor_wait(t):
641     """Make t do nothing (but loudly, if player avatar)."""
642     if t == world_db["Things"][0]:
643         strong_write(io_db["file_out"], "LOG You wait.\n")
644
645
646 def actor_move(t):
647     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
648     # # 7DRL: Player wounding (worse: killing) others will lower God's favor.
649     passable = False
650     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
651                                      t["T_POSY"], t["T_POSX"])
652     if 1 == move_result[0]:
653         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
654         passable = "." == chr(world_db["MAP"][pos])
655         hitted = [id for id in world_db["Things"]
656                   if world_db["Things"][id] != t
657                   if world_db["Things"][id]["T_LIFEPOINTS"]
658                   if world_db["Things"][id]["T_POSY"] == move_result[1]
659                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
660         if len(hitted):
661             hit_id = hitted[0]
662             if t == world_db["Things"][0]:
663                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
664                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
665                 strong_write(io_db["file_out"], "LOG You wound " + hitted_name
666                                                 + ".\n")
667                 add_gods_favor(-1)  # #
668             elif 0 == hit_id:
669                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
670                 strong_write(io_db["file_out"], "LOG " + hitter_name +
671                                                 " wounds you.\n")
672             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
673             if test and t == world_db["Things"][0]:  # #
674                 add_gods_favor(-test)  # #
675             return
676     dir = [dir for dir in directions_db
677            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
678     if passable:
679         t["T_POSY"] = move_result[1]
680         t["T_POSX"] = move_result[2]
681         for id in t["T_CARRIES"]:
682             world_db["Things"][id]["T_POSY"] = move_result[1]
683             world_db["Things"][id]["T_POSX"] = move_result[2]
684         build_fov_map(t)
685         if t == world_db["Things"][0]:
686             strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
687     elif t == world_db["Things"][0]:
688         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
689
690
691 def actor_pick_up(t):
692     """Make t pick up (topmost?) Thing from ground into inventory."""
693     # Topmostness is actually not defined so far. Picks Thing with highest ID.
694     used_slots = len(t["T_CARRIES"]) # #
695     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
696         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
697                if not world_db["Things"][id]["carried"]
698                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
699                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
700         if len(ids):
701             highest_id = 0
702             for id in ids:
703                 if id > highest_id:
704                     highest_id = id
705             world_db["Things"][highest_id]["carried"] = True
706             t["T_CARRIES"].append(highest_id)
707             if t == world_db["Things"][0]:
708                 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
709         elif t == world_db["Things"][0]:
710             err = "You try to pick up an object, but there is none."
711             strong_write(io_db["file_out"], "LOG " + err + "\n")
712     elif t == world_db["Things"][0]: # #
713         strong_write(io_db["file_out"], "LOG Can't pick up object: " + # #
714                                         "No storage room to carry more.\n") # #
715
716
717 def actor_drop(t):
718     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
719     # TODO: Handle case where T_ARGUMENT matches nothing.
720     if len(t["T_CARRIES"]):
721         id = t["T_CARRIES"][t["T_ARGUMENT"]]
722         t["T_CARRIES"].remove(id)
723         world_db["Things"][id]["carried"] = False
724         if t == world_db["Things"][0]:
725             strong_write(io_db["file_out"], "LOG You drop an object.\n")
726     elif t == world_db["Things"][0]:
727         err = "You try to drop an object, but you own none."
728         strong_write(io_db["file_out"], "LOG " + err + "\n")
729
730
731 def actor_use(t):
732     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
733     # TODO: Handle case where T_ARGUMENT matches nothing.
734     if len(t["T_CARRIES"]):
735         id = t["T_CARRIES"][t["T_ARGUMENT"]]
736         type = world_db["Things"][id]["T_TYPE"]
737         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
738             t["T_CARRIES"].remove(id)
739             del world_db["Things"][id]
740             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
741             if t == world_db["Things"][0]:
742                 strong_write(io_db["file_out"],
743                              "LOG You consume this object.\n")
744         elif t == world_db["Things"][0]:
745             strong_write(io_db["file_out"],
746                          "LOG You try to use this object, but fail.\n")
747     elif t == world_db["Things"][0]:
748         strong_write(io_db["file_out"],
749                      "LOG You try to use an object, but you own none.\n")
750
751
752 def thingproliferation(t):
753     """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
754
755     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
756     passable and not be inhabited by a Thing of the same type, or, if Thing is
757     animate, any other animate Thing. If there are several map cell candidates,
758     one is selected randomly.
759     """
760     # # 7DRL: success increments God's mood
761     def test_cell(t, y, x):
762         if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
763             for id in [id for id in world_db["Things"]
764                        if y == world_db["Things"][id]["T_POSY"]
765                        if x == world_db["Things"][id]["T_POSX"]
766                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
767                        or (t["T_LIFEPOINTS"] and
768                            world_db["Things"][id]["T_LIFEPOINTS"])]:
769                 return False
770             return True
771         return False
772     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
773     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
774         candidates = []
775         for dir in [directions_db[key] for key in directions_db]:
776             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
777             if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
778                 candidates.append((mv_result[1], mv_result[2]))
779         if len(candidates):
780             i = rand.next() % len(candidates)
781             id = id_setter(-1, "Things")
782             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
783             world_db["Things"][id] = newT
784             world_db["GOD_MOOD"] += 1  # #
785
786
787 def try_healing(t):
788     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
789
790     On success, decrease satiation score by 32.
791     """
792     # # 7DRL: Successful heals increment God's mood.
793     if t["T_SATIATION"] > 0 \
794        and t["T_LIFEPOINTS"] < \
795            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
796        and 0 == (rand.next() % 31) \
797        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
798                               if world_db["ThingActions"][id]["TA_NAME"] ==
799                                  "wait"][0]:
800         t["T_LIFEPOINTS"] += 1
801         world_db["GOD_MOOD"] += 1  # #
802         t["T_SATIATION"] -= 32
803         if t == world_db["Things"][0]:
804             strong_write(io_db["file_out"], "LOG You heal.\n")
805
806
807 def hunger(t):
808     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
809     if t["T_SATIATION"] > -32768:
810         t["T_SATIATION"] -= 1
811     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
812     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
813         raise RuntimeError("A thing that should not hunger is hungering.")
814     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
815     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
816         if t == world_db["Things"][0]:
817             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
818         decrement_lifepoints(t)
819
820
821 def get_dir_to_target(t, filter):
822     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
823
824     The path-wise nearest target is chosen, via the shortest available path.
825     Target must not be t. On succcess, return positive value, else False.
826     Filters:
827     "a": Thing in FOV is below a certain distance, animate, but of ThingType
828          that is not t's, and starts out weaker than t is; build path as
829          avoiding things of t's ThingType
830     "f": neighbor cell (not inhabited by any animate Thing) further away from
831          animate Thing not further than x steps away and in FOV and of a
832          ThingType that is not t's, and starts out stronger or as strong as t
833          is currently; or (cornered), if no such flight cell, but Thing of
834          above criteria is too near,1 a cell closer to it, or, if less near,
835          just wait
836     "c": Thing in memorized map is consumable
837     "s": memory map cell with greatest-reachable degree of unexploredness
838     """
839
840     def zero_score_map_where_char_on_memdepthmap(c):
841         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
842         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
843         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
844         #     set_map_score(i, 0)
845         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
846         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
847             raise RuntimeError("No score map allocated for "
848                                "zero_score_map_where_char_on_memdepthmap().")
849
850     def set_map_score(pos, score):
851         test = libpr.set_map_score(pos, score)
852         if test:
853             raise RuntimeError("No score map allocated for set_map_score().")
854
855     def get_map_score(pos):
856         result = libpr.get_map_score(pos)
857         if result < 0:
858             raise RuntimeError("No score map allocated for get_map_score().")
859         return result
860
861     def seeing_thing():
862         if t["fovmap"] and ("a" == filter or "f" == filter):
863             for id in world_db["Things"]:
864                 Thing = world_db["Things"][id]
865                 if Thing != t and Thing["T_LIFEPOINTS"] and \
866                    t["T_TYPE"] != Thing["T_TYPE"] and \
867                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
868                                           * world_db["MAP_LENGTH"])
869                                           + Thing["T_POSX"]]):
870                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
871                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
872                                           t["T_LIFEPOINTS"]) \
873                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
874                                              t["T_LIFEPOINTS"]):
875                         return True
876         elif t["T_MEMMAP"] and "c" == filter:
877             for mt in t["T_MEMTHING"]:
878                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
879                                          + mt[2]]) \
880                    and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
881                     return True
882         return False
883
884     def set_cells_passable_on_memmap_to_65534_on_scoremap():
885         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
886         # ord_dot = ord(".")
887         # memmap = t["T_MEMMAP"]
888         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
889         #            if ord_dot == memmap[i]]:
890         #     set_map_score(i, 65534) # i.e. 65535-1
891         map = c_pointer_to_bytearray(t["T_MEMMAP"])
892         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
893             raise RuntimeError("No score map allocated for "
894                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
895
896     def init_score_map():
897         test = libpr.init_score_map()
898         if test:
899             raise RuntimeError("Malloc error in init_score_map().")
900         ord_v = ord("v")
901         ord_blank = ord(" ")
902         set_cells_passable_on_memmap_to_65534_on_scoremap()
903         if "a" == filter:
904             for id in world_db["Things"]:
905                 Thing = world_db["Things"][id]
906                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
907                       + Thing["T_POSX"]
908                 if t != Thing and Thing["T_LIFEPOINTS"] and \
909                    t["T_TYPE"] != Thing["T_TYPE"] and \
910                    ord_v == t["fovmap"][pos] and \
911                    t["T_LIFEPOINTS"] > \
912                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
913                     set_map_score(pos, 0)
914                 elif t["T_TYPE"] == Thing["T_TYPE"]:
915                     set_map_score(pos, 65535)
916         elif "f" == filter:
917             for id in [id for id in world_db["Things"]
918                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
919                 Thing = world_db["Things"][id]
920                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
921                       + Thing["T_POSX"]
922                 if t["T_TYPE"] != Thing["T_TYPE"] and \
923                    ord_v == t["fovmap"][pos] and \
924                    t["T_LIFEPOINTS"] <= \
925                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
926                     set_map_score(pos, 0)
927         elif "c" == filter:
928             for mt in [mt for mt in t["T_MEMTHING"]
929                        if ord_blank != t["T_MEMMAP"][mt[1]
930                                                     * world_db["MAP_LENGTH"]
931                                                     + mt[2]]
932                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
933                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
934         elif "s" == filter:
935             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
936
937     def rand_target_dir(neighbors, cmp, dirs):
938         candidates = []
939         n_candidates = 0
940         for i in range(len(dirs)):
941             if cmp == neighbors[i]:
942                 candidates.append(dirs[i])
943                 n_candidates += 1
944         return candidates[rand.next() % n_candidates] if n_candidates else 0
945
946     def get_neighbor_scores(dirs, eye_pos):
947         scores = []
948         if libpr.ready_neighbor_scores(eye_pos):
949             raise RuntimeError("No score map allocated for " +
950                                "ready_neighbor_scores.()")
951         for i in range(len(dirs)):
952             scores.append(libpr.get_neighbor_score(i))
953         return scores
954
955     def get_dir_from_neighbors():
956         dir_to_target = False
957         dirs = "edcxsw"
958         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
959         neighbors = get_neighbor_scores(dirs, eye_pos)
960         if "f" == filter:
961             inhabited = [world_db["Things"][id]["T_POSY"]
962                          * world_db["MAP_LENGTH"]
963                          + world_db["Things"][id]["T_POSX"]
964                          for id in world_db["Things"]
965                          if world_db["Things"][id]["T_LIFEPOINTS"]]
966             for i in range(len(dirs)):
967                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
968                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
969                           + libpr.result_x()
970                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
971                     neighbors[i] = 65535
972                     break
973         minmax_start = 0 if "f" == filter else 65535 - 1
974         minmax_neighbor = minmax_start
975         for i in range(len(dirs)):
976             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
977                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
978                or ("f" != filter and minmax_neighbor > neighbors[i]):
979                 minmax_neighbor = neighbors[i]
980         if minmax_neighbor != minmax_start:
981             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
982         if "f" == filter:
983             if not dir_to_target:
984                 if 1 == get_map_score(eye_pos):
985                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
986                 elif 3 >= get_map_score(eye_pos):
987                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
988                                       if
989                                       world_db["ThingActions"][id]["TA_NAME"]
990                                          == "wait"][0]
991                     return 1
992             elif dir_to_target and 3 < get_map_score(eye_pos):
993                 dir_to_target = 0
994         elif "a" == filter and 10 <= get_map_score(eye_pos):
995             dir_to_target = 0
996         return dir_to_target
997
998     dir_to_target = False
999     mem_depth_c = b' '
1000     run_i = 9 + 1 if "s" == filter else 1
1001     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1002         run_i -= 1
1003         init_score_map()
1004         mem_depth_c = b'9' if b' ' == mem_depth_c \
1005                            else bytes([mem_depth_c[0] - 1])
1006         if libpr.dijkstra_map():
1007             raise RuntimeError("No score map allocated for dijkstra_map().")
1008         dir_to_target = get_dir_from_neighbors()
1009         libpr.free_score_map()
1010         if dir_to_target and str == type(dir_to_target):
1011             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1012                               if world_db["ThingActions"][id]["TA_NAME"]
1013                                  == "move"][0]
1014             t["T_ARGUMENT"] = ord(dir_to_target)
1015     return dir_to_target
1016
1017
1018 def standing_on_consumable(t):
1019     """Return True/False whether t is standing on a consumable."""
1020     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1021                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1022                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1023                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1024                           ["TT_CONSUMABLE"]]:
1025         return True
1026     return False
1027
1028
1029 def get_inventory_slot_to_consume(t):
1030     """Return slot Id of strongest consumable in t's inventory, else -1."""
1031     cmp_consumability = 0
1032     selection = -1
1033     i = 0
1034     for id in t["T_CARRIES"]:
1035         type = world_db["Things"][id]["T_TYPE"]
1036         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
1037             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
1038             selection = i
1039         i += 1
1040     return selection
1041
1042
1043 def ai(t):
1044     """Determine next command/argment for actor t via AI algorithms.
1045
1046     AI will look for, and move towards, enemies (animate Things not of their
1047     own ThingType); if they see none, they will consume consumables in their
1048     inventory; if there are none, they will pick up what they stand on if they
1049     stand on consumables; if they stand on none, they will move towards the
1050     next consumable they see or remember on the map; if they see or remember
1051     none, they will explore parts of the map unseen since ever or for at least
1052     one turn; if there is nothing to explore, they will simply wait.
1053     """
1054     # # 7DRL add: Don't pick up or search things when inventory is full.
1055     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1056                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1057     if not get_dir_to_target(t, "f"):
1058         sel = get_inventory_slot_to_consume(t)
1059         if -1 != sel:
1060             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1061                               if world_db["ThingActions"][id]["TA_NAME"]
1062                                  == "use"][0]
1063             t["T_ARGUMENT"] = sel
1064         elif standing_on_consumable(t) \
1065              and (len(t["T_CARRIES"]) < # #
1066                  world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]): # #
1067             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1068                               if world_db["ThingActions"][id]["TA_NAME"]
1069                                  == "pick_up"][0]
1070         elif (not
1071               (len(t["T_CARRIES"]) < # #
1072                 world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"] # #
1073                and get_dir_to_target(t, "c"))) and \
1074              (not get_dir_to_target(t, "a")):
1075             get_dir_to_target(t, "s")
1076
1077
1078 def turn_over():
1079     """Run game world and its inhabitants until new player input expected."""
1080     id = 0
1081     whilebreaker = False
1082     while world_db["Things"][0]["T_LIFEPOINTS"]:
1083         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1084             if not id in world_db["Things"] or \
1085                world_db["Things"][id]["carried"]:   # May have been consumed or
1086                 continue                            # picked up during turn …
1087             Thing = world_db["Things"][id]
1088             if Thing["T_LIFEPOINTS"]:
1089                 if not Thing["T_COMMAND"]:
1090                     update_map_memory(Thing)
1091                     if 0 == id:
1092                         whilebreaker = True
1093                         break
1094                     ai(Thing)
1095                 try_healing(Thing)
1096                 Thing["T_PROGRESS"] += 1
1097                 taid = [a for a in world_db["ThingActions"]
1098                           if a == Thing["T_COMMAND"]][0]
1099                 ThingAction = world_db["ThingActions"][taid]
1100                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1101                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
1102                     Thing["T_COMMAND"] = 0
1103                     Thing["T_PROGRESS"] = 0
1104                 hunger(Thing)
1105             thingproliferation(Thing)
1106         if whilebreaker:
1107             break
1108         world_db["TURN"] += 1
1109
1110
1111 def new_Thing(type, pos=(0, 0)):
1112     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1113     thing = {
1114         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1115         "T_ARGUMENT": 0,
1116         "T_PROGRESS": 0,
1117         "T_SATIATION": 0,
1118         "T_COMMAND": 0,
1119         "T_TYPE": type,
1120         "T_POSY": pos[0],
1121         "T_POSX": pos[1],
1122         "T_CARRIES": [],
1123         "carried": False,
1124         "T_MEMTHING": [],
1125         "T_MEMMAP": False,
1126         "T_MEMDEPTHMAP": False,
1127         "fovmap": False
1128     }
1129     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1130         build_fov_map(thing)
1131     return thing
1132
1133
1134 def id_setter(id, category, id_store=False, start_at_1=False):
1135     """Set ID of object of category to manipulate ID unused? Create new one.
1136
1137     The ID is stored as id_store.id (if id_store is set). If the integer of the
1138     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1139     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1140     always returned when no new object is created, otherwise the new object's
1141     ID.
1142     """
1143     min = 0 if start_at_1 else -1
1144     if str == type(id):
1145         id = integer_test(id, min)
1146     if None != id:
1147         if id in world_db[category]:
1148             if id_store:
1149                 id_store.id = id
1150             return None
1151         else:
1152             if (start_at_1 and 0 == id) \
1153                or ((not start_at_1) and (id < 0)):
1154                 id = 0 if start_at_1 else -1
1155                 while 1:
1156                     id = id + 1
1157                     if id not in world_db[category]:
1158                         break
1159             if id_store:
1160                 id_store.id = id
1161     return id
1162
1163
1164 def command_ping():
1165     """Send PONG line to server output file."""
1166     strong_write(io_db["file_out"], "PONG\n")
1167
1168
1169 def command_quit():
1170     """Abort server process."""
1171     if None == opts.replay:
1172         save_world()
1173         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1174     raise SystemExit("received QUIT command")
1175
1176
1177 def command_thingshere(str_y, str_x):
1178     """Write to out file list of Things known to player at coordinate y, x."""
1179     if world_db["WORLD_ACTIVE"]:
1180         y = integer_test(str_y, 0, 255)
1181         x = integer_test(str_x, 0, 255)
1182         length = world_db["MAP_LENGTH"]
1183         if None != y and None != x and y < length and x < length:
1184             pos = (y * world_db["MAP_LENGTH"]) + x
1185             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1186             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1187                 for id in world_db["Things"]:
1188                     if y == world_db["Things"][id]["T_POSY"] \
1189                        and x == world_db["Things"][id]["T_POSX"] \
1190                        and not world_db["Things"][id]["carried"]:
1191                         type = world_db["Things"][id]["T_TYPE"]
1192                         name = world_db["ThingTypes"][type]["TT_NAME"]
1193                         strong_write(io_db["file_out"], name + "\n")
1194             else:
1195                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1196                     if y == mt[1] and x == mt[2]:
1197                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1198                         strong_write(io_db["file_out"], name + "\n")
1199             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1200         else:
1201             print("Ignoring: Invalid map coordinates.")
1202     else:
1203         print("Ignoring: Command only works on existing worlds.")
1204
1205
1206 def play_commander(action, args=False):
1207     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1208
1209     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1210     """
1211
1212     def set_command():
1213         id = [x for x in world_db["ThingActions"]
1214                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1215         world_db["Things"][0]["T_COMMAND"] = id
1216         turn_over()
1217
1218     def set_command_and_argument_int(str_arg):
1219         val = integer_test(str_arg, 0, 255)
1220         if None != val:
1221             world_db["Things"][0]["T_ARGUMENT"] = val
1222             set_command()
1223
1224     def set_command_and_argument_movestring(str_arg):
1225         if str_arg in directions_db:
1226             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1227             set_command()
1228         else:
1229             print("Ignoring: Argument must be valid direction string.")
1230
1231     if action == "move":
1232         return set_command_and_argument_movestring
1233     elif args:
1234         return set_command_and_argument_int
1235     else:
1236         return set_command
1237
1238
1239 def command_seedrandomness(seed_string):
1240     """Set rand seed to int(seed_string)."""
1241     val = integer_test(seed_string, 0, 4294967295)
1242     if None != val:
1243         rand.seed = val
1244
1245
1246 def command_seedmap(seed_string):
1247     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
1248     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
1249     remake_map()
1250
1251
1252 def command_makeworld(seed_string):
1253     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1254
1255     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
1256     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
1257     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
1258     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1259     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1260     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1261     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1262     """
1263
1264     def free_pos():
1265         i = 0
1266         while 1:
1267             err = "Space to put thing on too hard to find. Map too small?"
1268             while 1:
1269                 y = rand.next() % world_db["MAP_LENGTH"]
1270                 x = rand.next() % world_db["MAP_LENGTH"]
1271                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1272                     break
1273                 i += 1
1274                 if i == 65535:
1275                     raise SystemExit(err)
1276             # Replica of C code, wrongly ignores animatedness of new Thing.
1277             pos_clear = (0 == len([id for id in world_db["Things"]
1278                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1279                                    if world_db["Things"][id]["T_POSY"] == y
1280                                    if world_db["Things"][id]["T_POSX"] == x]))
1281             if pos_clear:
1282                 break
1283         return (y, x)
1284
1285     val = integer_test(seed_string, 0, 4294967295)
1286     if None == val:
1287         return
1288     rand.seed = val
1289     world_db["SEED_MAP"] = val
1290     player_will_be_generated = False
1291     playertype = world_db["PLAYER_TYPE"]
1292     for ThingType in world_db["ThingTypes"]:
1293         if playertype == ThingType:
1294             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1295                 player_will_be_generated = True
1296             break
1297     if not player_will_be_generated:
1298         print("Ignoring beyond SEED_MAP: " +
1299               "No player type with start number >0 defined.")
1300         return
1301     wait_action = False
1302     for ThingAction in world_db["ThingActions"]:
1303         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1304             wait_action = True
1305     if not wait_action:
1306         print("Ignoring beyond SEED_MAP: " +
1307               "No thing action with name 'wait' defined.")
1308         return
1309     world_db["Things"] = {}
1310     remake_map()
1311     world_db["WORLD_ACTIVE"] = 1
1312     world_db["TURN"] = 1
1313     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1314         id = id_setter(-1, "Things")
1315         world_db["Things"][id] = new_Thing(playertype, free_pos())
1316     update_map_memory(world_db["Things"][0])
1317     for type in world_db["ThingTypes"]:
1318         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1319             if type != playertype:
1320                 id = id_setter(-1, "Things")
1321                 world_db["Things"][id] = new_Thing(type, free_pos())
1322     strong_write(io_db["file_out"], "NEW_WORLD\n")
1323
1324
1325 def command_maplength(maplength_string):
1326     """Redefine map length. Invalidate map, therefore lose all things on it."""
1327     val = integer_test(maplength_string, 1, 256)
1328     if None != val:
1329         world_db["MAP_LENGTH"] = val
1330         set_world_inactive()
1331         world_db["Things"] = {}
1332         libpr.set_maplength(val)
1333
1334
1335 def command_worldactive(worldactive_string):
1336     """Toggle world_db["WORLD_ACTIVE"] if possible.
1337
1338     An active world can always be set inactive. An inactive world can only be
1339     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1340     activation, rebuild all Things' FOVs, and the player's map memory.
1341     """
1342     # In original version, map existence was also tested (unnecessarily?).
1343     val = integer_test(worldactive_string, 0, 1)
1344     if val:
1345         if 0 != world_db["WORLD_ACTIVE"]:
1346             if 0 == val:
1347                 set_world_inactive()
1348             else:
1349                 print("World already active.")
1350         elif 0 == world_db["WORLD_ACTIVE"]:
1351             wait_exists = False
1352             for ThingAction in world_db["ThingActions"]:
1353                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1354                     wait_exists = True
1355                     break
1356             player_exists = False
1357             for Thing in world_db["Things"]:
1358                 if 0 == Thing:
1359                     player_exists = True
1360                     break
1361             if wait_exists and player_exists:
1362                 for id in world_db["Things"]:
1363                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1364                         build_fov_map(world_db["Things"][id])
1365                         if 0 == id:
1366                             update_map_memory(world_db["Things"][id], False)
1367                 world_db["WORLD_ACTIVE"] = 1
1368
1369
1370 def test_for_id_maker(object, category):
1371     """Return decorator testing for object having "id" attribute."""
1372     def decorator(f):
1373         def helper(*args):
1374             if hasattr(object, "id"):
1375                 f(*args)
1376             else:
1377                 print("Ignoring: No " + category +
1378                       " defined to manipulate yet.")
1379         return helper
1380     return decorator
1381
1382
1383 def command_tid(id_string):
1384     """Set ID of Thing to manipulate. ID unused? Create new one.
1385
1386     Default new Thing's type to the first available ThingType, others: zero.
1387     """
1388     id = id_setter(id_string, "Things", command_tid)
1389     if None != id:
1390         if world_db["ThingTypes"] == {}:
1391             print("Ignoring: No ThingType to settle new Thing in.")
1392             return
1393         type = list(world_db["ThingTypes"].keys())[0]
1394         world_db["Things"][id] = new_Thing(type)
1395
1396
1397 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1398
1399
1400 @test_Thing_id
1401 def command_tcommand(str_int):
1402     """Set T_COMMAND of selected Thing."""
1403     val = integer_test(str_int, 0)
1404     if None != val:
1405         if 0 == val or val in world_db["ThingActions"]:
1406             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1407         else:
1408             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1409
1410
1411 @test_Thing_id
1412 def command_ttype(str_int):
1413     """Set T_TYPE of selected Thing."""
1414     val = integer_test(str_int, 0)
1415     if None != val:
1416         if val in world_db["ThingTypes"]:
1417             world_db["Things"][command_tid.id]["T_TYPE"] = val
1418         else:
1419             print("Ignoring: ThingType ID belongs to no known ThingType.")
1420
1421
1422 @test_Thing_id
1423 def command_tcarries(str_int):
1424     """Append int(str_int) to T_CARRIES of selected Thing.
1425
1426     The ID int(str_int) must not be of the selected Thing, and must belong to a
1427     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1428     """
1429     val = integer_test(str_int, 0)
1430     if None != val:
1431         if val == command_tid.id:
1432             print("Ignoring: Thing cannot carry itself.")
1433         elif val in world_db["Things"] \
1434              and not world_db["Things"][val]["carried"]:
1435             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1436             world_db["Things"][val]["carried"] = True
1437         else:
1438             print("Ignoring: Thing not available for carrying.")
1439     # Note that the whole carrying structure is different from the C version:
1440     # Carried-ness is marked by a "carried" flag, not by Things containing
1441     # Things internally.
1442
1443
1444 @test_Thing_id
1445 def command_tmemthing(str_t, str_y, str_x):
1446     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1447
1448     The type must fit to an existing ThingType, and the position into the map.
1449     """
1450     type = integer_test(str_t, 0)
1451     posy = integer_test(str_y, 0, 255)
1452     posx = integer_test(str_x, 0, 255)
1453     if None != type and None != posy and None != posx:
1454         if type not in world_db["ThingTypes"] \
1455            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1456             print("Ignoring: Illegal value for thing type or position.")
1457         else:
1458             memthing = (type, posy, posx)
1459             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1460
1461
1462 def setter_map(maptype):
1463     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1464
1465     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1466     """
1467     @test_Thing_id
1468     def helper(str_int, mapline):
1469         val = integer_test(str_int, 0, 255)
1470         if None != val:
1471             if val >= world_db["MAP_LENGTH"]:
1472                 print("Illegal value for map line number.")
1473             elif len(mapline) != world_db["MAP_LENGTH"]:
1474                 print("Map line length is unequal map width.")
1475             else:
1476                 length = world_db["MAP_LENGTH"]
1477                 map = None
1478                 if not world_db["Things"][command_tid.id][maptype]:
1479                     map = bytearray(b' ' * (length ** 2))
1480                 else:
1481                     map = world_db["Things"][command_tid.id][maptype]
1482                 map[val * length:(val * length) + length] = mapline.encode()
1483                 world_db["Things"][command_tid.id][maptype] = map
1484     return helper
1485
1486
1487 def setter_tpos(axis):
1488     """Generate setter for T_POSX or  T_POSY of selected Thing.
1489
1490     If world is active, rebuilds animate things' fovmap, player's memory map.
1491     """
1492     @test_Thing_id
1493     def helper(str_int):
1494         val = integer_test(str_int, 0, 255)
1495         if None != val:
1496             if val < world_db["MAP_LENGTH"]:
1497                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1498                 if world_db["WORLD_ACTIVE"] \
1499                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1500                     build_fov_map(world_db["Things"][command_tid.id])
1501                     if 0 == command_tid.id:
1502                         update_map_memory(world_db["Things"][command_tid.id])
1503             else:
1504                 print("Ignoring: Position is outside of map.")
1505     return helper
1506
1507
1508 def command_ttid(id_string):
1509     """Set ID of ThingType to manipulate. ID unused? Create new one.
1510
1511     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1512     """
1513     id = id_setter(id_string, "ThingTypes", command_ttid)
1514     if None != id:
1515         world_db["ThingTypes"][id] = {
1516             "TT_NAME": "(none)",
1517             "TT_CONSUMABLE": 0,
1518             "TT_LIFEPOINTS": 0,
1519             "TT_PROLIFERATE": 0,
1520             "TT_START_NUMBER": 0,
1521             "TT_STORAGE": 0, # #
1522             "TT_SYMBOL": "?",
1523             "TT_CORPSE_ID": id
1524         }
1525
1526
1527 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1528
1529
1530 @test_ThingType_id
1531 def command_ttname(name):
1532     """Set TT_NAME of selected ThingType."""
1533     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1534
1535
1536 @test_ThingType_id
1537 def command_ttsymbol(char):
1538     """Set TT_SYMBOL of selected ThingType. """
1539     if 1 == len(char):
1540         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1541     else:
1542         print("Ignoring: Argument must be single character.")
1543
1544
1545 @test_ThingType_id
1546 def command_ttcorpseid(str_int):
1547     """Set TT_CORPSE_ID of selected ThingType."""
1548     val = integer_test(str_int, 0)
1549     if None != val:
1550         if val in world_db["ThingTypes"]:
1551             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1552         else:
1553             print("Ignoring: Corpse ID belongs to no known ThignType.")
1554
1555
1556 def command_taid(id_string):
1557     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1558
1559     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1560     """
1561     id = id_setter(id_string, "ThingActions", command_taid, True)
1562     if None != id:
1563         world_db["ThingActions"][id] = {
1564             "TA_EFFORT": 1,
1565             "TA_NAME": "wait"
1566         }
1567
1568
1569 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1570
1571
1572 @test_ThingAction_id
1573 def command_taname(name):
1574     """Set TA_NAME of selected ThingAction.
1575
1576     The name must match a valid thing action function. If after the name
1577     setting no ThingAction with name "wait" remains, call set_world_inactive().
1578     """
1579     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1580        or name == "pick_up":
1581         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1582         if 1 == world_db["WORLD_ACTIVE"]:
1583             wait_defined = False
1584             for id in world_db["ThingActions"]:
1585                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1586                     wait_defined = True
1587                     break
1588             if not wait_defined:
1589                 set_world_inactive()
1590     else:
1591         print("Ignoring: Invalid action name.")
1592     # In contrast to the original,naming won't map a function to a ThingAction.
1593
1594
1595 def command_ai():
1596     """Call ai() on player Thing, then turn_over()."""
1597     ai(world_db["Things"][0])
1598     turn_over()
1599
1600
1601 """Commands database.
1602
1603 Map command start tokens to ([0]) number of expected command arguments, ([1])
1604 the command's meta-ness (i.e. is it to be written to the record file, is it to
1605 be ignored in replay mode if read from server input file), and ([2]) a function
1606 to be called on it.
1607 """
1608 commands_db = {
1609     "QUIT": (0, True, command_quit),
1610     "PING": (0, True, command_ping),
1611     "THINGS_HERE": (2, True, command_thingshere),
1612     "MAKE_WORLD": (1, False, command_makeworld),
1613     "SEED_MAP": (1, False, command_seedmap),
1614     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1615     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1616     "GOD_MOOD": (1, False, setter(None, "GOD_MOOD", -32768, 32767)),  # #
1617     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
1618     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1619     "MAP_LENGTH": (1, False, command_maplength),
1620     "WORLD_ACTIVE": (1, False, command_worldactive),
1621     "TA_ID": (1, False, command_taid),
1622     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1623     "TA_NAME": (1, False, command_taname),
1624     "TT_ID": (1, False, command_ttid),
1625     "TT_NAME": (1, False, command_ttname),
1626     "TT_SYMBOL": (1, False, command_ttsymbol),
1627     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1628     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1629                                        0, 65535)),
1630     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1631                                          0, 255)),
1632     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1633                                         0, 255)),
1634     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1635     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)), # #
1636     "T_ID": (1, False, command_tid),
1637     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1638     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1639     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1640     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1641     "T_COMMAND": (1, False, command_tcommand),
1642     "T_TYPE": (1, False, command_ttype),
1643     "T_CARRIES": (1, False, command_tcarries),
1644     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1645     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1646     "T_MEMTHING": (3, False, command_tmemthing),
1647     "T_POSY": (1, False, setter_tpos("Y")),
1648     "T_POSX": (1, False, setter_tpos("X")),
1649     "wait": (0, False, play_commander("wait")),
1650     "move": (1, False, play_commander("move")),
1651     "pick_up": (0, False, play_commander("pick_up")),
1652     "drop": (1, False, play_commander("drop", True)),
1653     "use": (1, False, play_commander("use", True)),
1654     "ai": (0, False, command_ai)
1655 }
1656
1657
1658 """World state database. With sane default values. (Randomness is in rand.)"""
1659 world_db = {
1660     "TURN": 0,
1661     "MAP_LENGTH": 64,
1662     "SEED_MAP": 0,
1663     "PLAYER_TYPE": 0,
1664     "WORLD_ACTIVE": 0,
1665     "GOD_MOOD": 0,  # #
1666     "GOD_FAVOR": 0,  # #
1667     "ThingActions": {},
1668     "ThingTypes": {},
1669     "Things": {}
1670 }
1671
1672 """Mapping of direction names to internal direction chars."""
1673 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1674                  "west": "s", "north-west": "w", "north-east": "e"}
1675
1676 """File IO database."""
1677 io_db = {
1678     "path_save": "save",
1679     "path_record": "record_save",
1680     "path_worldconf": "confserver/world",
1681     "path_server": "server/",
1682     "path_in": "server/in",
1683     "path_out": "server/out",
1684     "path_worldstate": "server/worldstate",
1685     "tmp_suffix": "_tmp",
1686     "kicked_by_rival": False,
1687     "worldstate_updateable": False
1688 }
1689
1690
1691 try:
1692     libpr = prep_library()
1693     rand = RandomnessIO()
1694     opts = parse_command_line_arguments()
1695     if opts.savefile:
1696         io_db["path_save"] = opts.savefile
1697         io_db["path_record"] = "record_" + opts.savefile
1698     setup_server_io()
1699     if opts.verbose:
1700         io_db["verbose"] = True
1701     if None != opts.replay:
1702         replay_game()
1703     else:
1704         play_game()
1705 except SystemExit as exit:
1706     print("ABORTING: " + exit.args[0])
1707 except:
1708     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1709     raise
1710 finally:
1711     cleanup_server_io()