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