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