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