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