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