home · contact · privacy
Add log_help(), short usage info sent to log on server start.
[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     # 7DRL: Also add some ":" cells, and (not surrounded by trees!) "_" altar.
431
432     def is_neighbor(coordinates, type):
433         y = coordinates[0]
434         x = coordinates[1]
435         length = world_db["MAP_LENGTH"]
436         ind = y % 2
437         diag_west = x + (ind > 0)
438         diag_east = x + (ind < (length - 1))
439         pos = (y * length) + x
440         if (y > 0 and diag_east
441             and type == chr(world_db["MAP"][pos - length + ind])) \
442            or (x < (length - 1)
443                and type == chr(world_db["MAP"][pos + 1])) \
444            or (y < (length - 1) and diag_east
445                and type == chr(world_db["MAP"][pos + length + ind])) \
446            or (y > 0 and diag_west
447                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
448            or (x > 0
449                and type == chr(world_db["MAP"][pos - 1])) \
450            or (y < (length - 1) and diag_west
451                and type == chr(world_db["MAP"][pos + length - (not ind)])):
452             return True
453         return False
454
455     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
456     length = world_db["MAP_LENGTH"]
457     add_half_width = (not (length % 2)) * int(length / 2)
458     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
459     while (1):
460         y = rand.next() % length
461         x = rand.next() % length
462         pos = (y * length) + x
463         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
464             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
465                 break
466             world_db["MAP"][pos] = ord(".")
467     n_trees = int((length ** 2) / 16)
468     i_trees = 0
469     while (i_trees <= n_trees):
470         single_allowed = rand.next() % 32
471         y = rand.next() % length
472         x = rand.next() % length
473         pos = (y * length) + x
474         if "." == chr(world_db["MAP"][pos]) \
475           and ((not single_allowed) or is_neighbor((y, x), "X")):
476             world_db["MAP"][pos] = ord("X")
477             i_trees += 1
478     # This all-too-precise replica of the original C code misses iter_limit().
479     n_colons = int((length ** 2) / 16)  # #
480     i_colons = 0  # #
481     while (i_colons <= n_colons):  # #
482         single_allowed = rand.next() % 256  # #
483         y = rand.next() % length  # #
484         x = rand.next() % length  # #
485         pos = (y * length) + x  # #
486         if ("." == chr(world_db["MAP"][pos])  # #
487           and ((not single_allowed) or is_neighbor((y, x), ":"))):  # #
488             world_db["MAP"][pos] = ord(":")  # #
489             i_colons += 1  # #
490     altar_placed = False  # #
491     while not altar_placed:  # #
492         y = rand.next() % length  # #
493         x = rand.next() % length  # #
494         pos = (y * length) + x  # #
495         if (("." == chr(world_db["MAP"][pos]  # #
496              or ":" == chr(world_db["MAP"][pos]))
497             and not is_neighbor((y, x), 'X'))):  # #
498             world_db["MAP"][pos] = ord("_")  # #
499             world_db["altar"] = (y, x)  # #
500             altar_placed = True  # #
501
502
503 def update_map_memory(t, age_map=True):
504     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
505
506     def age_some_memdepthmap_on_nonfov_cells():
507         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
508         # ord_v = ord("v")
509         # ord_0 = ord("0")
510         # ord_9 = ord("9")
511         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
512         #             if not ord_v == t["fovmap"][pos]
513         #             if ord_0 <= t["T_MEMDEPTHMAP"][pos]
514         #             if ord_9 > t["T_MEMDEPTHMAP"][pos]
515         #             if not rand.next() % (2 **
516         #                                   (t["T_MEMDEPTHMAP"][pos] - 48))]:
517         #     t["T_MEMDEPTHMAP"][pos] += 1
518         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
519         fovmap = c_pointer_to_bytearray(t["fovmap"])
520         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
521
522     if not t["T_MEMMAP"]:
523         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
524     if not t["T_MEMDEPTHMAP"]:
525         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
526     ord_v = ord("v")
527     ord_0 = ord("0")
528     ord_space = ord(" ")
529     for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
530                 if ord_v == t["fovmap"][pos]]:
531         t["T_MEMDEPTHMAP"][pos] = ord_0
532         if ord_space == t["T_MEMMAP"][pos]:
533             t["T_MEMMAP"][pos] = world_db["MAP"][pos]
534     if age_map:
535         age_some_memdepthmap_on_nonfov_cells()
536     t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
537                        if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
538                                                + mt[2]]]
539     for id in [id for id in world_db["Things"]
540                if not world_db["Things"][id]["carried"]]:
541         type = world_db["Things"][id]["T_TYPE"]
542         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
543             y = world_db["Things"][id]["T_POSY"]
544             x = world_db["Things"][id]["T_POSX"]
545             if ord_v == t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]:
546                 t["T_MEMTHING"].append((type, y, x))
547
548
549 def set_world_inactive():
550     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
551     server_test()
552     if os.access(io_db["path_worldstate"], os.F_OK):
553         os.remove(io_db["path_worldstate"])
554     world_db["WORLD_ACTIVE"] = 0
555
556
557 def integer_test(val_string, min, max=None):
558     """Return val_string if possible integer >= min and <= max, else None."""
559     try:
560         val = int(val_string)
561         if val < min or (max is not None and val > max):
562             raise ValueError
563         return val
564     except ValueError:
565         msg = "Ignoring: Please use integer >= " + str(min)
566         if max is not None:
567             msg += " and <= " + str(max)
568         msg += "."
569         print(msg)
570         return None
571
572
573 def setter(category, key, min, max=None):
574     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
575     if category is None:
576         def f(val_string):
577             val = integer_test(val_string, min, max)
578             if None != val:
579                 world_db[key] = val
580     else:
581         if category == "Thing":
582             id_store = command_tid
583             decorator = test_Thing_id
584         elif category == "ThingType":
585             id_store = command_ttid
586             decorator = test_ThingType_id
587         elif category == "ThingAction":
588             id_store = command_taid
589             decorator = test_ThingAction_id
590
591         @decorator
592         def f(val_string):
593             val = integer_test(val_string, min, max)
594             if None != val:
595                 world_db[category + "s"][id_store.id][key] = val
596     return f
597
598
599 def build_fov_map(t):
600     """Build Thing's FOV map."""
601     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
602     fovmap = c_pointer_to_bytearray(t["fovmap"])
603     map = c_pointer_to_bytearray(world_db["MAP"])
604     if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
605         raise RuntimeError("Malloc error in build_fov_Map().")
606
607
608 def log_help():
609     """Send quick usage info to log."""
610     strong_write(io_db["file_out"], "LOG "
611                  + "Use 'w'/'e'/'s'/'d'/'x'/'c' to move.\n")
612     strong_write(io_db["file_out"], "LOG "
613                  + "Use 'p' to pick up objects, and 'D' to drop them.\n")
614     strong_write(io_db["file_out"], "LOG "
615                  + "Some objects can be used (such as: eaten) by 'u' if "
616                  + "they are in your inventory. "
617                  + "Use 'UP'/'DOWN' to navigate the inventory.\n")
618     strong_write(io_db["file_out"], "LOG "
619                  + "Use 'l' to toggle 'look' mode (move an exploration cursor "
620                  + "instead of the player over the map).\n")
621     strong_write(io_db["file_out"], "LOG See README file for more details.\n")
622
623
624 def decrement_lifepoints(t):
625     """Decrement t's lifepoints by 1, and if to zero, corpse it.
626
627     If t is the player avatar, only blank its fovmap, so that the client may
628     still display memory data. On non-player things, erase fovmap and memory.
629     Dying actors drop all their things.
630     """
631     # 7DRL: Also decrements God's mood; deaths heavily so.
632     # 7DRL: Return 1 if death, else 0.
633     t["T_LIFEPOINTS"] -= 1
634     world_db["GOD_MOOD"] -= 1  # #
635     if 0 == t["T_LIFEPOINTS"]:
636         sadness = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
637         world_db["GOD_MOOD"] -= sadness  # #        
638         for id in t["T_CARRIES"]:
639             t["T_CARRIES"].remove(id)
640             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
641             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
642             world_db["Things"][id]["carried"] = False
643         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
644         if world_db["Things"][0] == t:
645             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
646             strong_write(io_db["file_out"], "LOG You die.\n")
647         else:
648             t["fovmap"] = False
649             t["T_MEMMAP"] = False
650             t["T_MEMDEPTHMAP"] = False
651             t["T_MEMTHING"] = []
652         return sadness  # #
653     return 0  # #
654
655
656 def add_gods_favor(i): # #
657     """"Add to GOD_FAVOR, multiplied with factor growing log. with GOD_MOOD."""
658     def favor_multiplier(i):
659         x = 100
660         threshold = math.e * x
661         mood = world_db["GOD_MOOD"]
662         if i > 0:
663             if mood > threshold:
664                 i = i * math.log(mood / x)
665             elif -mood > threshold:
666                 i = i / math.log(-mood / x)
667         elif i < 0:
668             if -mood > threshold:
669                 i = i * math.log(-mood / x)
670             if mood > threshold:
671                 i = i / math.log(mood / x)
672         return int(i)
673     world_db["GOD_FAVOR"] += favor_multiplier(i)
674
675
676 def mv_yx_in_dir_legal(dir, y, x):
677     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
678     dir_c = dir.encode("ascii")[0]
679     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
680     if -1 == test:
681         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
682     return (test, libpr.result_y(), libpr.result_x())
683
684
685 def enter_altar():  # #
686      """What happens when the player enters the altar."""
687      if world_db["GAME_WON"]:
688         strong_write(io_db["file_out"],
689                      "LOG You step on a soul-less slab of stone.\n")
690         return
691      strong_write(io_db["file_out"], "LOG YOU ENTER SACRED GROUND.\n")
692      if world_db["GOD_FAVOR"] > 9000:
693          world_db["GAME_WON"] = 1
694          strong_write(io_db["file_out"], "LOG The Island God speaks to you: "
695                       + "\"You have proven yourself worthy of my respect. "
696                       + "You were a good citizen to the island, and sometimes "
697                       + "a better steward to its inhabitants than me. The "
698                       + "island shall miss you when you leave. But you have "
699                       + "earned the right to do so. Take this "
700                       + world_db["ThingTypes"][world_db["SLIPPERS"]]["TT_NAME"]
701                       + " and USE it when you please. It will take you to "
702                       + "where you came from. (But do feel free to stay here "
703                       + "as long as you like.)\"\n")
704          id = id_setter(-1, "Things")
705          world_db["Things"][id] = new_Thing(world_db["SLIPPERS"],
706                                             world_db["altar"])
707
708
709 def actor_wait(t):
710     """Make t do nothing (but loudly, if player avatar)."""
711     if t == world_db["Things"][0]:
712         strong_write(io_db["file_out"], "LOG You wait.\n")
713
714
715 def actor_move(t):
716     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
717     # 7DRL: Player wounding (worse: killing) others will lower God's favor.
718     # 7DRL: Player entering the altar triggers enter_altar().
719     passable = False
720     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
721                                      t["T_POSY"], t["T_POSX"])
722     if 1 == move_result[0]:
723         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
724         passable = ("." == chr(world_db["MAP"][pos]) or
725                     ":" == chr(world_db["MAP"][pos]) or # #
726                     "_" == chr(world_db["MAP"][pos])) # #
727         hitted = [id for id in world_db["Things"]
728                   if world_db["Things"][id] != t
729                   if world_db["Things"][id]["T_LIFEPOINTS"]
730                   if world_db["Things"][id]["T_POSY"] == move_result[1]
731                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
732         if len(hitted):
733             hit_id = hitted[0]
734             if t == world_db["Things"][0]:
735                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
736                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
737                 strong_write(io_db["file_out"], "LOG You wound " + hitted_name
738                                                 + ".\n")
739                 add_gods_favor(-1)  # #
740             elif 0 == hit_id:
741                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
742                 strong_write(io_db["file_out"], "LOG " + hitter_name +
743                                                 " wounds you.\n")
744             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
745             if test and t == world_db["Things"][0]:  # #
746                 add_gods_favor(-test)  # #
747             return
748     dir = [dir for dir in directions_db
749            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
750     if passable:
751         t["T_POSY"] = move_result[1]
752         t["T_POSX"] = move_result[2]
753         for id in t["T_CARRIES"]:
754             world_db["Things"][id]["T_POSY"] = move_result[1]
755             world_db["Things"][id]["T_POSX"] = move_result[2]
756         build_fov_map(t)
757         if t == world_db["Things"][0]:
758             strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
759             if (move_result[1] == world_db["altar"][0] and  # #
760                 move_result[2] == world_db["altar"][1]):  # #
761                 enter_altar()  # #
762     elif t == world_db["Things"][0]:
763         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
764
765
766 def actor_pick_up(t):
767     """Make t pick up (topmost?) Thing from ground into inventory."""
768     # Topmostness is actually not defined so far. Picks most nutritious Thing.
769     # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
770     used_slots = len(t["T_CARRIES"]) # #
771     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
772         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
773                if not world_db["Things"][id]["carried"]
774                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
775                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
776         if len(ids):
777             highest_id = ids[0]
778             nutritious = 0
779             for id in ids:
780                 type = world_db["Things"][id]["T_TYPE"]
781                 if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > nutritious:
782                     nutritious = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
783                     highest_id = id
784             world_db["Things"][highest_id]["carried"] = True
785             if (t != world_db["Things"][0] and  # #
786                 world_db["Things"][highest_id]["T_PLAYERDROP"]):  # #
787                 x = world_db["Things"][highest_id]["T_TYPE"]
788                 score = world_db["ThingTypes"][x]["TT_CONSUMABLE"] / 32  # #
789                 add_gods_favor(score)  # #
790                 world_db["Things"][highest_id]["T_PLAYERDROP"] = 0  # #
791             t["T_CARRIES"].append(highest_id)
792             if t == world_db["Things"][0]:
793                 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
794         elif t == world_db["Things"][0]:
795             err = "You try to pick up an object, but there is none."
796             strong_write(io_db["file_out"], "LOG " + err + "\n")
797     elif t == world_db["Things"][0]: # #
798         strong_write(io_db["file_out"], "LOG Can't pick up object: " + # #
799                                         "No storage room to carry more.\n") # #
800
801
802 def actor_drop(t):
803     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
804     # TODO: Handle case where T_ARGUMENT matches nothing.
805     if len(t["T_CARRIES"]):
806         id = t["T_CARRIES"][t["T_ARGUMENT"]]
807         t["T_CARRIES"].remove(id)
808         world_db["Things"][id]["carried"] = False
809         if t == world_db["Things"][0]:
810             strong_write(io_db["file_out"], "LOG You drop an object.\n")
811             world_db["Things"][id]["T_PLAYERDROP"] = 1  # #
812     elif t == world_db["Things"][0]:
813         err = "You try to drop an object, but you own none."
814         strong_write(io_db["file_out"], "LOG " + err + "\n")
815
816
817 def actor_use(t):
818     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
819     # TODO: Handle case where T_ARGUMENT matches nothing.
820     # 7DLR: Handle SLIPPERS-type Thing use.
821     if len(t["T_CARRIES"]):
822         id = t["T_CARRIES"][t["T_ARGUMENT"]]
823         type = world_db["Things"][id]["T_TYPE"]
824         if type == world_db["SLIPPERS"]:  # #
825             if t == world_db["Things"][0]:  # #
826                 strong_write(io_db["file_out"], "LOG You use the "  # #
827                              + world_db["ThingTypes"][type]["TT_NAME"]  # #
828                              + ". It glows in wondrous colors, and emits "  # #
829                              + "a sound as if from a dying cat. The "  # #
830                              + "Island God laughs.\n")  # #
831             t["T_LIFEPOINTS"] = 1  # #
832             decrement_lifepoints(t)  # #
833         elif world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
834             t["T_CARRIES"].remove(id)
835             del world_db["Things"][id]
836             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
837             if t == world_db["Things"][0]:
838                 strong_write(io_db["file_out"],
839                              "LOG You consume this object.\n")
840         elif t == world_db["Things"][0]:
841             strong_write(io_db["file_out"],
842                          "LOG You try to use this object, but fail.\n")
843     elif t == world_db["Things"][0]:
844         strong_write(io_db["file_out"],
845                      "LOG You try to use an object, but you own none.\n")
846
847
848 def thingproliferation(t, prol_map):
849     """To chance of 1/TT_PROLIFERATE,create  t offspring in open neighbor cell.
850
851     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
852     marked "." in prol_map. If there are several map cell candidates, one is
853     selected randomly.
854     """
855     # 7DRL: success increments God's mood
856     # 7DRL: Things proliferate only on ":" ground.
857     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
858     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
859         candidates = []
860         for dir in [directions_db[key] for key in directions_db]:
861             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
862             if mv_result[0] and ord(":") == prol_map[mv_result[1]  # #
863             # if mv_result[0] and ord(".") == prol_map[mv_result[1]
864                                                      * world_db["MAP_LENGTH"]
865                                                      + mv_result[2]]:
866                 candidates.append((mv_result[1], mv_result[2]))
867         if len(candidates):
868             i = rand.next() % len(candidates)
869             id = id_setter(-1, "Things")
870             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
871             world_db["Things"][id] = newT
872             world_db["GOD_MOOD"] += 1  # #
873
874
875 def try_healing(t):
876     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
877
878     On success, decrease satiation score by 32.
879     """
880     # 7DRL: Successful heals increment God's mood.
881     if t["T_SATIATION"] > 0 \
882        and t["T_LIFEPOINTS"] < \
883            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
884        and 0 == (rand.next() % 31) \
885        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
886                               if world_db["ThingActions"][id]["TA_NAME"] ==
887                                  "wait"][0]:
888         t["T_LIFEPOINTS"] += 1
889         world_db["GOD_MOOD"] += 1  # #
890         t["T_SATIATION"] -= 32
891         if t == world_db["Things"][0]:
892             strong_write(io_db["file_out"], "LOG You heal.\n")
893
894
895 def hunger(t):
896     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
897     if t["T_SATIATION"] > -32768:
898         t["T_SATIATION"] -= 1
899     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
900     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
901         raise RuntimeError("A thing that should not hunger is hungering.")
902     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
903     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
904         if t == world_db["Things"][0]:
905             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
906         decrement_lifepoints(t)
907
908
909 def get_dir_to_target(t, filter):
910     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
911
912     The path-wise nearest target is chosen, via the shortest available path.
913     Target must not be t. On succcess, return positive value, else False.
914     Filters:
915     "a": Thing in FOV is below a certain distance, animate, but of ThingType
916          that is not t's, and starts out weaker than t is; build path as
917          avoiding things of t's ThingType
918     "f": neighbor cell (not inhabited by any animate Thing) further away from
919          animate Thing not further than x steps away and in FOV and of a
920          ThingType that is not t's, and starts out stronger or as strong as t
921          is currently; or (cornered), if no such flight cell, but Thing of
922          above criteria is too near,1 a cell closer to it, or, if less near,
923          just wait
924     "c": Thing in memorized map is consumable
925     "s": memory map cell with greatest-reachable degree of unexploredness
926     """
927
928     def zero_score_map_where_char_on_memdepthmap(c):
929         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
930         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
931         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
932         #     set_map_score(i, 0)
933         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
934         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
935             raise RuntimeError("No score map allocated for "
936                                "zero_score_map_where_char_on_memdepthmap().")
937
938     def set_map_score(pos, score):
939         test = libpr.set_map_score(pos, score)
940         if test:
941             raise RuntimeError("No score map allocated for set_map_score().")
942
943     def get_map_score(pos):
944         result = libpr.get_map_score(pos)
945         if result < 0:
946             raise RuntimeError("No score map allocated for get_map_score().")
947         return result
948
949     def seeing_thing():
950         if t["fovmap"] and ("a" == filter or "f" == filter):
951             for id in world_db["Things"]:
952                 Thing = world_db["Things"][id]
953                 if Thing != t and Thing["T_LIFEPOINTS"] and \
954                    t["T_TYPE"] != Thing["T_TYPE"] and \
955                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
956                                           * world_db["MAP_LENGTH"])
957                                           + Thing["T_POSX"]]):
958                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
959                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
960                                           t["T_LIFEPOINTS"]) \
961                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
962                                              t["T_LIFEPOINTS"]):
963                         return True
964         elif t["T_MEMMAP"] and "c" == filter:
965             for mt in t["T_MEMTHING"]:
966                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
967                                          + mt[2]]) \
968                    and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
969                     return True
970         return False
971
972     def set_cells_passable_on_memmap_to_65534_on_scoremap():
973         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
974         # ord_dot = ord(".")
975         # memmap = t["T_MEMMAP"]
976         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
977         #            if ord_dot == memmap[i]]:
978         #     set_map_score(i, 65534) # i.e. 65535-1
979         map = c_pointer_to_bytearray(t["T_MEMMAP"])
980         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
981             raise RuntimeError("No score map allocated for "
982                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
983
984     def init_score_map():
985         test = libpr.init_score_map()
986         if test:
987             raise RuntimeError("Malloc error in init_score_map().")
988         ord_v = ord("v")
989         ord_blank = ord(" ")
990         set_cells_passable_on_memmap_to_65534_on_scoremap()
991         if "a" == filter:
992             for id in world_db["Things"]:
993                 Thing = world_db["Things"][id]
994                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
995                       + Thing["T_POSX"]
996                 if t != Thing and Thing["T_LIFEPOINTS"] and \
997                    t["T_TYPE"] != Thing["T_TYPE"] and \
998                    ord_v == t["fovmap"][pos] and \
999                    t["T_LIFEPOINTS"] > \
1000                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
1001                     set_map_score(pos, 0)
1002                 elif t["T_TYPE"] == Thing["T_TYPE"]:
1003                     set_map_score(pos, 65535)
1004         elif "f" == filter:
1005             for id in [id for id in world_db["Things"]
1006                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
1007                 Thing = world_db["Things"][id]
1008                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
1009                       + Thing["T_POSX"]
1010                 if t["T_TYPE"] != Thing["T_TYPE"] and \
1011                    ord_v == t["fovmap"][pos] and \
1012                    t["T_LIFEPOINTS"] <= \
1013                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
1014                     set_map_score(pos, 0)
1015         elif "c" == filter:
1016             for mt in [mt for mt in t["T_MEMTHING"]
1017                        if ord_blank != t["T_MEMMAP"][mt[1]
1018                                                     * world_db["MAP_LENGTH"]
1019                                                     + mt[2]]
1020                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
1021                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
1022         elif "s" == filter:
1023             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
1024
1025     def rand_target_dir(neighbors, cmp, dirs):
1026         candidates = []
1027         n_candidates = 0
1028         for i in range(len(dirs)):
1029             if cmp == neighbors[i]:
1030                 candidates.append(dirs[i])
1031                 n_candidates += 1
1032         return candidates[rand.next() % n_candidates] if n_candidates else 0
1033
1034     def get_neighbor_scores(dirs, eye_pos):
1035         scores = []
1036         if libpr.ready_neighbor_scores(eye_pos):
1037             raise RuntimeError("No score map allocated for " +
1038                                "ready_neighbor_scores.()")
1039         for i in range(len(dirs)):
1040             scores.append(libpr.get_neighbor_score(i))
1041         return scores
1042
1043     def get_dir_from_neighbors():
1044         dir_to_target = False
1045         dirs = "edcxsw"
1046         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1047         neighbors = get_neighbor_scores(dirs, eye_pos)
1048         if "f" == filter:
1049             inhabited = [world_db["Things"][id]["T_POSY"]
1050                          * world_db["MAP_LENGTH"]
1051                          + world_db["Things"][id]["T_POSX"]
1052                          for id in world_db["Things"]
1053                          if world_db["Things"][id]["T_LIFEPOINTS"]]
1054             for i in range(len(dirs)):
1055                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
1056                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
1057                           + libpr.result_x()
1058                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
1059                     neighbors[i] = 65535
1060                     break
1061         minmax_start = 0 if "f" == filter else 65535 - 1
1062         minmax_neighbor = minmax_start
1063         for i in range(len(dirs)):
1064             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
1065                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1066                or ("f" != filter and minmax_neighbor > neighbors[i]):
1067                 minmax_neighbor = neighbors[i]
1068         if minmax_neighbor != minmax_start:
1069             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1070         if "f" == filter:
1071             if not dir_to_target:
1072                 if 1 == get_map_score(eye_pos):
1073                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
1074                 elif 3 >= get_map_score(eye_pos):
1075                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1076                                       if
1077                                       world_db["ThingActions"][id]["TA_NAME"]
1078                                          == "wait"][0]
1079                     return 1
1080             elif dir_to_target and 3 < get_map_score(eye_pos):
1081                 dir_to_target = 0
1082         elif "a" == filter and 10 <= get_map_score(eye_pos):
1083             dir_to_target = 0
1084         return dir_to_target
1085
1086     dir_to_target = False
1087     mem_depth_c = b' '
1088     run_i = 9 + 1 if "s" == filter else 1
1089     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1090         run_i -= 1
1091         init_score_map()
1092         mem_depth_c = b'9' if b' ' == mem_depth_c \
1093                            else bytes([mem_depth_c[0] - 1])
1094         if libpr.dijkstra_map():
1095             raise RuntimeError("No score map allocated for dijkstra_map().")
1096         dir_to_target = get_dir_from_neighbors()
1097         libpr.free_score_map()
1098         if dir_to_target and str == type(dir_to_target):
1099             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1100                               if world_db["ThingActions"][id]["TA_NAME"]
1101                                  == "move"][0]
1102             t["T_ARGUMENT"] = ord(dir_to_target)
1103     return dir_to_target
1104
1105
1106 def standing_on_consumable(t):
1107     """Return True/False whether t is standing on a consumable."""
1108     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1109                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1110                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1111                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1112                           ["TT_CONSUMABLE"]]:
1113         return True
1114     return False
1115
1116
1117 def get_inventory_slot_to_consume(t):
1118     """Return slot Id of strongest consumable in t's inventory, else -1."""
1119     cmp_consumability = 0
1120     selection = -1
1121     i = 0
1122     for id in t["T_CARRIES"]:
1123         type = world_db["Things"][id]["T_TYPE"]
1124         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
1125             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
1126             selection = i
1127         i += 1
1128     return selection
1129
1130
1131 def ai(t):
1132     """Determine next command/argment for actor t via AI algorithms.
1133
1134     AI will look for, and move towards, enemies (animate Things not of their
1135     own ThingType); if they see none, they will consume consumables in their
1136     inventory; if there are none, they will pick up what they stand on if they
1137     stand on consumables; if they stand on none, they will move towards the
1138     next consumable they see or remember on the map; if they see or remember
1139     none, they will explore parts of the map unseen since ever or for at least
1140     one turn; if there is nothing to explore, they will simply wait.
1141     """
1142     # 7DRL add: Don't pick up or search things when inventory is full.
1143     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1144                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1145     if not get_dir_to_target(t, "f"):
1146         sel = get_inventory_slot_to_consume(t)
1147         if -1 != sel:
1148             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1149                               if world_db["ThingActions"][id]["TA_NAME"]
1150                                  == "use"][0]
1151             t["T_ARGUMENT"] = sel
1152         elif standing_on_consumable(t) \
1153              and (len(t["T_CARRIES"]) < # #
1154                  world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]): # #
1155             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1156                               if world_db["ThingActions"][id]["TA_NAME"]
1157                                  == "pick_up"][0]
1158         elif (not
1159               (len(t["T_CARRIES"]) < # #
1160                 world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"] # #
1161                and get_dir_to_target(t, "c"))) and \
1162              (not get_dir_to_target(t, "a")):
1163             get_dir_to_target(t, "s")
1164
1165
1166 def turn_over():
1167     """Run game world and its inhabitants until new player input expected."""
1168     id = 0
1169     whilebreaker = False
1170     while world_db["Things"][0]["T_LIFEPOINTS"]:
1171         proliferable_map = world_db["MAP"][:]
1172         for id in [id for id in world_db["Things"]
1173                    if not world_db["Things"][id]["carried"]]:
1174             y = world_db["Things"][id]["T_POSY"]
1175             x = world_db["Things"][id]["T_POSX"]
1176             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1177         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1178             if not id in world_db["Things"] or \
1179                world_db["Things"][id]["carried"]:   # May have been consumed or
1180                 continue                            # picked up during turn …
1181             Thing = world_db["Things"][id]
1182             if Thing["T_LIFEPOINTS"]:
1183                 if not Thing["T_COMMAND"]:
1184                     update_map_memory(Thing)
1185                     if 0 == id:
1186                         whilebreaker = True
1187                         break
1188                     ai(Thing)
1189                 try_healing(Thing)
1190                 hunger(Thing)
1191                 if Thing["T_LIFEPOINTS"]:
1192                     Thing["T_PROGRESS"] += 1
1193                     taid = [a for a in world_db["ThingActions"]
1194                               if a == Thing["T_COMMAND"]][0]
1195                     ThingAction = world_db["ThingActions"][taid]
1196                     if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1197                         eval("actor_" + ThingAction["TA_NAME"])(Thing)
1198                         Thing["T_COMMAND"] = 0
1199                         Thing["T_PROGRESS"] = 0
1200             thingproliferation(Thing, proliferable_map)
1201         if whilebreaker:
1202             break
1203         world_db["TURN"] += 1
1204
1205
1206 def new_Thing(type, pos=(0, 0)):
1207     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1208     thing = {
1209         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1210         "T_ARGUMENT": 0,
1211         "T_PROGRESS": 0,
1212         "T_SATIATION": 0,
1213         "T_COMMAND": 0,
1214         "T_PLAYERDROP": 0,  # #
1215         "T_TYPE": type,
1216         "T_POSY": pos[0],
1217         "T_POSX": pos[1],
1218         "T_CARRIES": [],
1219         "carried": False,
1220         "T_MEMTHING": [],
1221         "T_MEMMAP": False,
1222         "T_MEMDEPTHMAP": False,
1223         "fovmap": False
1224     }
1225     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1226         build_fov_map(thing)
1227     return thing
1228
1229
1230 def id_setter(id, category, id_store=False, start_at_1=False):
1231     """Set ID of object of category to manipulate ID unused? Create new one.
1232
1233     The ID is stored as id_store.id (if id_store is set). If the integer of the
1234     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1235     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1236     always returned when no new object is created, otherwise the new object's
1237     ID.
1238     """
1239     min = 0 if start_at_1 else -1
1240     if str == type(id):
1241         id = integer_test(id, min)
1242     if None != id:
1243         if id in world_db[category]:
1244             if id_store:
1245                 id_store.id = id
1246             return None
1247         else:
1248             if (start_at_1 and 0 == id) \
1249                or ((not start_at_1) and (id < 0)):
1250                 id = 0 if start_at_1 else -1
1251                 while 1:
1252                     id = id + 1
1253                     if id not in world_db[category]:
1254                         break
1255             if id_store:
1256                 id_store.id = id
1257     return id
1258
1259
1260 def command_ping():
1261     """Send PONG line to server output file."""
1262     strong_write(io_db["file_out"], "PONG\n")
1263
1264
1265 def command_quit():
1266     """Abort server process."""
1267     if None == opts.replay:
1268         if world_db["WORLD_ACTIVE"]:
1269             save_world()
1270         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1271     raise SystemExit("received QUIT command")
1272
1273
1274 def command_thingshere(str_y, str_x):
1275     """Write to out file list of Things known to player at coordinate y, x."""
1276     if world_db["WORLD_ACTIVE"]:
1277         y = integer_test(str_y, 0, 255)
1278         x = integer_test(str_x, 0, 255)
1279         length = world_db["MAP_LENGTH"]
1280         if None != y and None != x and y < length and x < length:
1281             pos = (y * world_db["MAP_LENGTH"]) + x
1282             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1283             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1284                 for id in world_db["Things"]:
1285                     if y == world_db["Things"][id]["T_POSY"] \
1286                        and x == world_db["Things"][id]["T_POSX"] \
1287                        and not world_db["Things"][id]["carried"]:
1288                         type = world_db["Things"][id]["T_TYPE"]
1289                         name = world_db["ThingTypes"][type]["TT_NAME"]
1290                         strong_write(io_db["file_out"], name + "\n")
1291             else:
1292                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1293                     if y == mt[1] and x == mt[2]:
1294                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1295                         strong_write(io_db["file_out"], name + "\n")
1296             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1297         else:
1298             print("Ignoring: Invalid map coordinates.")
1299     else:
1300         print("Ignoring: Command only works on existing worlds.")
1301
1302
1303 def play_commander(action, args=False):
1304     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1305
1306     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1307     """
1308
1309     def set_command():
1310         id = [x for x in world_db["ThingActions"]
1311                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1312         world_db["Things"][0]["T_COMMAND"] = id
1313         turn_over()
1314
1315     def set_command_and_argument_int(str_arg):
1316         val = integer_test(str_arg, 0, 255)
1317         if None != val:
1318             world_db["Things"][0]["T_ARGUMENT"] = val
1319             set_command()
1320
1321     def set_command_and_argument_movestring(str_arg):
1322         if str_arg in directions_db:
1323             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1324             set_command()
1325         else:
1326             print("Ignoring: Argument must be valid direction string.")
1327
1328     if action == "move":
1329         return set_command_and_argument_movestring
1330     elif args:
1331         return set_command_and_argument_int
1332     else:
1333         return set_command
1334
1335
1336 def command_seedrandomness(seed_string):
1337     """Set rand seed to int(seed_string)."""
1338     val = integer_test(seed_string, 0, 4294967295)
1339     if None != val:
1340         rand.seed = val
1341
1342
1343 def command_makeworld(seed_string):
1344     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1345
1346     Seed rand with seed. Do more only with a "wait" ThingAction and
1347     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1348     world_db["Things"] emptied, call make_map() and set
1349     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1350     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1351     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1352     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1353     Call log_help().
1354     """
1355
1356     # def free_pos(plant=False):
1357     def free_pos(plant=False):  # #
1358         i = 0
1359         while 1:
1360             err = "Space to put thing on too hard to find. Map too small?"
1361             while 1:
1362                 y = rand.next() % world_db["MAP_LENGTH"]
1363                 x = rand.next() % world_db["MAP_LENGTH"]
1364                 pos = y * world_db["MAP_LENGTH"] + x;
1365                 if (not plant  # #
1366                     and "." == chr(world_db["MAP"][pos])) \
1367                    or ":" == chr(world_db["MAP"][pos]):  # #
1368                     break
1369                 i += 1
1370                 if i == 65535:
1371                     raise SystemExit(err)
1372             # Replica of C code, wrongly ignores animatedness of new Thing.
1373             pos_clear = (0 == len([id for id in world_db["Things"]
1374                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1375                                    if world_db["Things"][id]["T_POSY"] == y
1376                                    if world_db["Things"][id]["T_POSX"] == x]))
1377             if pos_clear:
1378                 break
1379         return (y, x)
1380
1381     val = integer_test(seed_string, 0, 4294967295)
1382     if None == val:
1383         return
1384     rand.seed = val
1385     player_will_be_generated = False
1386     playertype = world_db["PLAYER_TYPE"]
1387     for ThingType in world_db["ThingTypes"]:
1388         if playertype == ThingType:
1389             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1390                 player_will_be_generated = True
1391             break
1392     if not player_will_be_generated:
1393         print("Ignoring: No player type with start number >0 defined.")
1394         return
1395     wait_action = False
1396     for ThingAction in world_db["ThingActions"]:
1397         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1398             wait_action = True
1399     if not wait_action:
1400         print("Ignoring: No thing action with name 'wait' defined.")
1401         return
1402     if not world_db["SLIPPERS"] in world_db["ThingTypes"]:  # #
1403         print("Ignoring: No valid SLIPPERS set.")  # #
1404         return  # #
1405     world_db["Things"] = {}
1406     make_map()
1407     world_db["WORLD_ACTIVE"] = 1
1408     world_db["TURN"] = 1
1409     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1410         id = id_setter(-1, "Things")
1411         world_db["Things"][id] = new_Thing(playertype, free_pos())
1412     if not world_db["Things"][0]["fovmap"]:
1413         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1414         world_db["Things"][0]["fovmap"] = empty_fovmap
1415     update_map_memory(world_db["Things"][0])
1416     for type in world_db["ThingTypes"]:
1417         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1418             if type != playertype:
1419                 id = id_setter(-1, "Things")
1420                 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"]  # #
1421                 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1422     strong_write(io_db["file_out"], "NEW_WORLD\n")
1423     log_help()
1424
1425
1426 def command_maplength(maplength_string):
1427     """Redefine map length. Invalidate map, therefore lose all things on it."""
1428     val = integer_test(maplength_string, 1, 256)
1429     if None != val:
1430         world_db["MAP_LENGTH"] = val
1431         world_db["MAP"] = False
1432         set_world_inactive()
1433         world_db["Things"] = {}
1434         libpr.set_maplength(val)
1435
1436
1437 def command_worldactive(worldactive_string):
1438     """Toggle world_db["WORLD_ACTIVE"] if possible.
1439
1440     An active world can always be set inactive. An inactive world can only be
1441     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1442     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1443     Also call log_help().
1444     """
1445     # 7DRL: altar must be on map, and (valid) SLIPPERS must be set for world
1446     # activation.
1447     val = integer_test(worldactive_string, 0, 1)
1448     if None != val:
1449         if 0 != world_db["WORLD_ACTIVE"]:
1450             if 0 == val:
1451                 set_world_inactive()
1452             else:
1453                 print("World already active.")
1454         elif 0 == world_db["WORLD_ACTIVE"]:
1455             wait_exists = False
1456             for ThingAction in world_db["ThingActions"]:
1457                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1458                     wait_exists = True
1459                     break
1460             player_exists = False
1461             for Thing in world_db["Things"]:
1462                 if 0 == Thing:
1463                     player_exists = True
1464                     break
1465             valid_slippers = world_db["SLIPPERS"] in world_db["ThingTypes"]  # #
1466             altar_found = False  # #
1467             if world_db["MAP"]:  # #
1468                 pos = world_db["MAP"].find(b'_')  # #
1469                 if pos > 0:  # #
1470                     y = int(pos / world_db["MAP_LENGTH"])  # #
1471                     x = pos % world_db["MAP_LENGTH"]  # #
1472                     world_db["altar"] = (y, x)  # #
1473                     altar_found = True  # #
1474             if wait_exists and player_exists and world_db["MAP"] \
1475                 and altar_found and valid_slippers:  # #
1476                 for id in world_db["Things"]:
1477                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1478                         build_fov_map(world_db["Things"][id])
1479                         if 0 == id:
1480                             update_map_memory(world_db["Things"][id], False)
1481                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1482                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1483                     world_db["Things"][0]["fovmap"] = empty_fovmap
1484                 world_db["WORLD_ACTIVE"] = 1
1485                 log_help()
1486             else:
1487                 print("Ignoring: Not all conditions for world activation met.")
1488
1489
1490 def command_slippers(str_int):  # #
1491     """Set SLIPPERS, but deactivate world if not in ThingTypes."""
1492     val = integer_test(str_int, 0)
1493     if None != val:
1494         world_db["SLIPPERS"] = val
1495         if world_db["WORLD_ACTIVE"] and \
1496            world_db["SLIPPERS"] not in world_db["ThingTypes"]:
1497             world_db["WORLD_ACTIVE"] = 0
1498             print("SLIPPERS matches no known ThingTypes, deactivating world.")
1499
1500
1501 def test_for_id_maker(object, category):
1502     """Return decorator testing for object having "id" attribute."""
1503     def decorator(f):
1504         def helper(*args):
1505             if hasattr(object, "id"):
1506                 f(*args)
1507             else:
1508                 print("Ignoring: No " + category +
1509                       " defined to manipulate yet.")
1510         return helper
1511     return decorator
1512
1513
1514 def command_tid(id_string):
1515     """Set ID of Thing to manipulate. ID unused? Create new one.
1516
1517     Default new Thing's type to the first available ThingType, others: zero.
1518     """
1519     id = id_setter(id_string, "Things", command_tid)
1520     if None != id:
1521         if world_db["ThingTypes"] == {}:
1522             print("Ignoring: No ThingType to settle new Thing in.")
1523             return
1524         type = list(world_db["ThingTypes"].keys())[0]
1525         world_db["Things"][id] = new_Thing(type)
1526
1527
1528 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1529
1530
1531 @test_Thing_id
1532 def command_tcommand(str_int):
1533     """Set T_COMMAND of selected Thing."""
1534     val = integer_test(str_int, 0)
1535     if None != val:
1536         if 0 == val or val in world_db["ThingActions"]:
1537             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1538         else:
1539             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1540
1541
1542 @test_Thing_id
1543 def command_ttype(str_int):
1544     """Set T_TYPE of selected Thing."""
1545     val = integer_test(str_int, 0)
1546     if None != val:
1547         if val in world_db["ThingTypes"]:
1548             world_db["Things"][command_tid.id]["T_TYPE"] = val
1549         else:
1550             print("Ignoring: ThingType ID belongs to no known ThingType.")
1551
1552
1553 @test_Thing_id
1554 def command_tcarries(str_int):
1555     """Append int(str_int) to T_CARRIES of selected Thing.
1556
1557     The ID int(str_int) must not be of the selected Thing, and must belong to a
1558     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1559     """
1560     val = integer_test(str_int, 0)
1561     if None != val:
1562         if val == command_tid.id:
1563             print("Ignoring: Thing cannot carry itself.")
1564         elif val in world_db["Things"] \
1565              and not world_db["Things"][val]["carried"]:
1566             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1567             world_db["Things"][val]["carried"] = True
1568         else:
1569             print("Ignoring: Thing not available for carrying.")
1570     # Note that the whole carrying structure is different from the C version:
1571     # Carried-ness is marked by a "carried" flag, not by Things containing
1572     # Things internally.
1573
1574
1575 @test_Thing_id
1576 def command_tmemthing(str_t, str_y, str_x):
1577     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1578
1579     The type must fit to an existing ThingType, and the position into the map.
1580     """
1581     type = integer_test(str_t, 0)
1582     posy = integer_test(str_y, 0, 255)
1583     posx = integer_test(str_x, 0, 255)
1584     if None != type and None != posy and None != posx:
1585         if type not in world_db["ThingTypes"] \
1586            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1587             print("Ignoring: Illegal value for thing type or position.")
1588         else:
1589             memthing = (type, posy, posx)
1590             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1591
1592
1593 def setter_map(maptype):
1594     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1595
1596     If no map of maptype exists yet, initialize it with ' ' bytes first.
1597     """
1598
1599     def valid_map_line(str_int, mapline):
1600         val = integer_test(str_int, 0, 255)
1601         if None != val:
1602             if val >= world_db["MAP_LENGTH"]:
1603                 print("Illegal value for map line number.")
1604             elif len(mapline) != world_db["MAP_LENGTH"]:
1605                 print("Map line length is unequal map width.")
1606             else:
1607                 return val
1608         return None
1609
1610     def nonThingMap_helper(str_int, mapline):
1611         val = valid_map_line(str_int, mapline)
1612         if None != val:
1613             length = world_db["MAP_LENGTH"]
1614             if not world_db["MAP"]:
1615                 map = bytearray(b' ' * (length ** 2))
1616             else:
1617                 map = world_db["MAP"]
1618             map[val * length:(val * length) + length] = mapline.encode()
1619             if not world_db["MAP"]:
1620                 world_db["MAP"] = map
1621
1622     @test_Thing_id
1623     def ThingMap_helper(str_int, mapline):
1624         val = valid_map_line(str_int, mapline)
1625         if None != val:
1626             length = world_db["MAP_LENGTH"]
1627             if not world_db["Things"][command_tid.id][maptype]:
1628                 map = bytearray(b' ' * (length ** 2))
1629             else:
1630                 map = world_db["Things"][command_tid.id][maptype]
1631             map[val * length:(val * length) + length] = mapline.encode()
1632             if not world_db["Things"][command_tid.id][maptype]:
1633                 world_db["Things"][command_tid.id][maptype] = map
1634
1635     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1636
1637
1638 def setter_tpos(axis):
1639     """Generate setter for T_POSX or  T_POSY of selected Thing.
1640
1641     If world is active, rebuilds animate things' fovmap, player's memory map.
1642     """
1643     @test_Thing_id
1644     def helper(str_int):
1645         val = integer_test(str_int, 0, 255)
1646         if None != val:
1647             if val < world_db["MAP_LENGTH"]:
1648                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1649                 if world_db["WORLD_ACTIVE"] \
1650                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1651                     build_fov_map(world_db["Things"][command_tid.id])
1652                     if 0 == command_tid.id:
1653                         update_map_memory(world_db["Things"][command_tid.id])
1654             else:
1655                 print("Ignoring: Position is outside of map.")
1656     return helper
1657
1658
1659 def command_ttid(id_string):
1660     """Set ID of ThingType to manipulate. ID unused? Create new one.
1661
1662     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1663     """
1664     id = id_setter(id_string, "ThingTypes", command_ttid)
1665     if None != id:
1666         world_db["ThingTypes"][id] = {
1667             "TT_NAME": "(none)",
1668             "TT_CONSUMABLE": 0,
1669             "TT_LIFEPOINTS": 0,
1670             "TT_PROLIFERATE": 0,
1671             "TT_START_NUMBER": 0,
1672             "TT_STORAGE": 0, # #
1673             "TT_SYMBOL": "?",
1674             "TT_CORPSE_ID": id
1675         }
1676
1677
1678 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1679
1680
1681 @test_ThingType_id
1682 def command_ttname(name):
1683     """Set TT_NAME of selected ThingType."""
1684     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1685
1686
1687 @test_ThingType_id
1688 def command_ttsymbol(char):
1689     """Set TT_SYMBOL of selected ThingType. """
1690     if 1 == len(char):
1691         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1692     else:
1693         print("Ignoring: Argument must be single character.")
1694
1695
1696 @test_ThingType_id
1697 def command_ttcorpseid(str_int):
1698     """Set TT_CORPSE_ID of selected ThingType."""
1699     val = integer_test(str_int, 0)
1700     if None != val:
1701         if val in world_db["ThingTypes"]:
1702             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1703         else:
1704             print("Ignoring: Corpse ID belongs to no known ThignType.")
1705
1706
1707 def command_taid(id_string):
1708     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1709
1710     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1711     """
1712     id = id_setter(id_string, "ThingActions", command_taid, True)
1713     if None != id:
1714         world_db["ThingActions"][id] = {
1715             "TA_EFFORT": 1,
1716             "TA_NAME": "wait"
1717         }
1718
1719
1720 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1721
1722
1723 @test_ThingAction_id
1724 def command_taname(name):
1725     """Set TA_NAME of selected ThingAction.
1726
1727     The name must match a valid thing action function. If after the name
1728     setting no ThingAction with name "wait" remains, call set_world_inactive().
1729     """
1730     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1731        or name == "pick_up":
1732         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1733         if 1 == world_db["WORLD_ACTIVE"]:
1734             wait_defined = False
1735             for id in world_db["ThingActions"]:
1736                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1737                     wait_defined = True
1738                     break
1739             if not wait_defined:
1740                 set_world_inactive()
1741     else:
1742         print("Ignoring: Invalid action name.")
1743     # In contrast to the original,naming won't map a function to a ThingAction.
1744
1745
1746 def command_ai():
1747     """Call ai() on player Thing, then turn_over()."""
1748     ai(world_db["Things"][0])
1749     turn_over()
1750
1751
1752 """Commands database.
1753
1754 Map command start tokens to ([0]) number of expected command arguments, ([1])
1755 the command's meta-ness (i.e. is it to be written to the record file, is it to
1756 be ignored in replay mode if read from server input file), and ([2]) a function
1757 to be called on it.
1758 """
1759 commands_db = {
1760     "QUIT": (0, True, command_quit),
1761     "PING": (0, True, command_ping),
1762     "THINGS_HERE": (2, True, command_thingshere),
1763     "MAKE_WORLD": (1, False, command_makeworld),
1764     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1765     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1766     "GOD_MOOD": (1, False, setter(None, "GOD_MOOD", -32768, 32767)),  # #
1767     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
1768     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1769     "MAP_LENGTH": (1, False, command_maplength),
1770     "WORLD_ACTIVE": (1, False, command_worldactive),
1771     "MAP": (2, False, setter_map("MAP")),
1772     "GAME_WON": (1, False, setter(None, "GAME_WON", 0, 1)),  # #
1773     "SLIPPERS": (1, False, command_slippers),  # #
1774     "TA_ID": (1, False, command_taid),
1775     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1776     "TA_NAME": (1, False, command_taname),
1777     "TT_ID": (1, False, command_ttid),
1778     "TT_NAME": (1, False, command_ttname),
1779     "TT_SYMBOL": (1, False, command_ttsymbol),
1780     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1781     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1782                                        0, 65535)),
1783     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1784                                          0, 255)),
1785     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1786                                         0, 255)),
1787     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1788     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)),  # #
1789     "T_ID": (1, False, command_tid),
1790     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1791     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1792     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1793     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1794     "T_COMMAND": (1, False, command_tcommand),
1795     "T_TYPE": (1, False, command_ttype),
1796     "T_CARRIES": (1, False, command_tcarries),
1797     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1798     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1799     "T_MEMTHING": (3, False, command_tmemthing),
1800     "T_POSY": (1, False, setter_tpos("Y")),
1801     "T_POSX": (1, False, setter_tpos("X")),
1802     "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)),  # #
1803     "wait": (0, False, play_commander("wait")),
1804     "move": (1, False, play_commander("move")),
1805     "pick_up": (0, False, play_commander("pick_up")),
1806     "drop": (1, False, play_commander("drop", True)),
1807     "use": (1, False, play_commander("use", True)),
1808     "ai": (0, False, command_ai)
1809 }
1810 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1811
1812
1813 """World state database. With sane default values. (Randomness is in rand.)"""
1814 world_db = {
1815     "TURN": 0,
1816     "MAP_LENGTH": 64,
1817     "PLAYER_TYPE": 0,
1818     "WORLD_ACTIVE": 0,
1819     "GOD_MOOD": 0,  # #
1820     "GOD_FAVOR": 0,  # #
1821     "MAP": False,
1822     "GAME_WON": 0,  # #
1823     "SLIPPERS": 0,  # #
1824     "ThingActions": {},
1825     "ThingTypes": {},
1826     "Things": {}
1827 }
1828
1829 """Mapping of direction names to internal direction chars."""
1830 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1831                  "west": "s", "north-west": "w", "north-east": "e"}
1832
1833 """File IO database."""
1834 io_db = {
1835     "path_save": "save",
1836     "path_record": "record_save",
1837     "path_worldconf": "confserver/world",
1838     "path_server": "server/",
1839     "path_in": "server/in",
1840     "path_out": "server/out",
1841     "path_worldstate": "server/worldstate",
1842     "tmp_suffix": "_tmp",
1843     "kicked_by_rival": False,
1844     "worldstate_updateable": False
1845 }
1846
1847
1848 try:
1849     libpr = prep_library()
1850     rand = RandomnessIO()
1851     opts = parse_command_line_arguments()
1852     if opts.savefile:
1853         io_db["path_save"] = opts.savefile
1854         io_db["path_record"] = "record_" + opts.savefile
1855     setup_server_io()
1856     if opts.verbose:
1857         io_db["verbose"] = True
1858     if None != opts.replay:
1859         replay_game()
1860     else:
1861         play_game()
1862 except SystemExit as exit:
1863     print("ABORTING: " + exit.args[0])
1864 except:
1865     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1866     raise
1867 finally:
1868     cleanup_server_io()