home · contact · privacy
7DRL: Add some more documentation.
[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 log(msg):
103     """Send "msg" to log."""
104     strong_write(io_db["file_out"], "LOG " + msg + "\n")
105
106
107 def obey(command, prefix, replay=False, do_record=False):
108     """Call function from commands_db mapped to command's first token.
109
110     Tokenize command string with shlex.split(comments=True). If replay is set,
111     a non-meta command from the commands_db merely triggers obey() on the next
112     command from the records file. If not, non-meta commands set
113     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
114     do_record is set, are recorded to io_db["record_chunk"], and save_world()
115     is called (and io_db["record_chunk"] written) if 15 seconds have passed
116     since the last time it was called. The prefix string is inserted into the
117     server's input message between its beginning 'input ' and ':'. All activity
118     is preceded by a server_test() call. Commands that start with a lowercase
119     letter are ignored when world_db["WORLD_ACTIVE"] is False/0.
120     """
121     server_test()
122     if io_db["verbose"]:
123         print("input " + prefix + ": " + command)
124     try:
125         tokens = shlex.split(command, comments=True)
126     except ValueError as err:
127         print("Can't tokenize command string: " + str(err) + ".")
128         return
129     if len(tokens) > 0 and tokens[0] in commands_db \
130        and len(tokens) == commands_db[tokens[0]][0] + 1:
131         if commands_db[tokens[0]][1]:
132             commands_db[tokens[0]][2](*tokens[1:])
133         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
134             print("Ignoring lowercase-starting commands when world inactive.")
135         elif replay:
136             print("Due to replay mode, reading command as 'go on in record'.")
137             line = io_db["file_record"].readline()
138             if len(line) > 0:
139                 obey(line.rstrip(), io_db["file_record"].prefix
140                      + str(io_db["file_record"].line_n))
141                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
142             else:
143                 print("Reached end of record file.")
144         else:
145             commands_db[tokens[0]][2](*tokens[1:])
146             if do_record:
147                 io_db["record_chunk"] += command + "\n"
148                 if time.time() > io_db["save_wait"] + 300:
149                     atomic_write(io_db["path_record"], io_db["record_chunk"],
150                                  do_append=True)
151                     if world_db["WORLD_ACTIVE"]:
152                         save_world()
153                     io_db["record_chunk"] = ""
154                     io_db["save_wait"] = time.time()
155             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
156     elif 0 != len(tokens):
157         print("Invalid command/argument, or bad number of tokens.")
158
159
160 def atomic_write(path, text, do_append=False, delete=True):
161     """Atomic write of text to file at path, appended if do_append is set."""
162     path_tmp = path + io_db["tmp_suffix"]
163     mode = "w"
164     if do_append:
165         mode = "a"
166         if os.access(path, os.F_OK):
167             shutil.copyfile(path, path_tmp)
168     file = open(path_tmp, mode)
169     strong_write(file, text)
170     file.close()
171     if delete and os.access(path, os.F_OK):
172         os.remove(path)
173     os.rename(path_tmp, path)
174
175
176 def save_world():
177     """Save all commands needed to reconstruct current world state."""
178
179     def quote(string):
180         string = string.replace("\u005C", '\u005C\u005C')
181         return '"' + string.replace('"', '\u005C"') + '"'
182
183     def mapsetter(key):
184         def helper(id=None):
185             string = ""
186             if key == "MAP" or world_db["Things"][id][key]:
187                 map = world_db["MAP"] if key == "MAP" \
188                                       else world_db["Things"][id][key]
189                 length = world_db["MAP_LENGTH"]
190                 for i in range(length):
191                     line = map[i * length:(i * length) + length].decode()
192                     string = string + key + " " + str(i) + " " + quote(line) \
193                              + "\n"
194             return string
195         return helper
196
197     def memthing(id):
198         string = ""
199         for memthing in world_db["Things"][id]["T_MEMTHING"]:
200             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
201                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
202         return string
203
204     def helper(category, id_string, special_keys={}):
205         string = ""
206         for id in world_db[category]:
207             string = string + id_string + " " + str(id) + "\n"
208             for key in world_db[category][id]:
209                 if not key in special_keys:
210                     x = world_db[category][id][key]
211                     argument = quote(x) if str == type(x) else str(x)
212                     string = string + key + " " + argument + "\n"
213                 elif special_keys[key]:
214                     string = string + special_keys[key](id)
215         return string
216
217     string = ""
218     for key in world_db:
219         if (dict != type(world_db[key])
220             and key != "altar"  # #
221             and key != "MAP" and key != "WORLD_ACTIVE"):
222             string = string + key + " " + str(world_db[key]) + "\n"
223     string = string + mapsetter("MAP")()
224     string = string + helper("ThingActions", "TA_ID")
225     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
226     for id in world_db["ThingTypes"]:
227         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
228                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
229     string = string + helper("Things", "T_ID",
230                              {"T_CARRIES": False, "carried": False,
231                               "T_MEMMAP": mapsetter("T_MEMMAP"),
232                               "T_MEMTHING": memthing, "fovmap": False,
233                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
234     for id in world_db["Things"]:
235         if [] != world_db["Things"][id]["T_CARRIES"]:
236             string = string + "T_ID " + str(id) + "\n"
237             for carried_id in world_db["Things"][id]["T_CARRIES"]:
238                 string = string + "T_CARRIES " + str(carried_id) + "\n"
239     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
240              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
241     atomic_write(io_db["path_save"], string)
242
243
244 def obey_lines_in_file(path, name, do_record=False):
245     """Call obey() on each line of path's file, use name in input prefix."""
246     file = open(path, "r")
247     line_n = 1
248     for line in file.readlines():
249         obey(line.rstrip(), name + "file line " + str(line_n),
250              do_record=do_record)
251         line_n = line_n + 1
252     file.close()
253
254
255 def parse_command_line_arguments():
256     """Return settings values read from command line arguments."""
257     parser = argparse.ArgumentParser()
258     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
259                         action='store')
260     parser.add_argument('-l', nargs="?", const="save", dest='savefile',
261                         action="store")
262     parser.add_argument('-v', dest='verbose', action='store_true')
263     opts, unknown = parser.parse_known_args()
264     return opts
265
266
267 def server_test():
268     """Ensure valid server out file belonging to current process.
269
270     This is done by comparing io_db["teststring"] to what's found at the start
271     of the current file at io_db["path_out"]. On failure, set
272     io_db["kicked_by_rival"] and raise SystemExit.
273     """
274     if not os.access(io_db["path_out"], os.F_OK):
275         raise SystemExit("Server output file has disappeared.")
276     file = open(io_db["path_out"], "r")
277     test = file.readline().rstrip("\n")
278     file.close()
279     if test != io_db["teststring"]:
280         io_db["kicked_by_rival"] = True
281         msg = "Server test string in server output file does not match. This" \
282               " indicates that the current server process has been " \
283               "superseded by another one."
284         raise SystemExit(msg)
285
286
287 def read_command():
288     """Return next newline-delimited command from server in file.
289
290     Keep building return string until a newline is encountered. Pause between
291     unsuccessful reads, and after too much waiting, run server_test().
292     """
293     wait_on_fail = 0.03333
294     max_wait = 5
295     now = time.time()
296     command = ""
297     while True:
298         add = io_db["file_in"].readline()
299         if len(add) > 0:
300             command = command + add
301             if len(command) > 0 and "\n" == command[-1]:
302                 command = command[:-1]
303                 break
304         else:
305             time.sleep(wait_on_fail)
306             if now + max_wait < time.time():
307                 server_test()
308                 now = time.time()
309     return command
310
311
312 def try_worldstate_update():
313     """Write worldstate file if io_db["worldstate_updateable"] is set."""
314     if io_db["worldstate_updateable"]:
315
316         def write_map(string, map):
317             for i in range(length):
318                 line = map[i * length:(i * length) + length].decode()
319                 string = string + line + "\n"
320             return string
321
322         inventory = ""
323         if [] == world_db["Things"][0]["T_CARRIES"]:
324             inventory = "(none)\n"
325         else:
326             for id in world_db["Things"][0]["T_CARRIES"]:
327                 type_id = world_db["Things"][id]["T_TYPE"]
328                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
329                 inventory = inventory + name + "\n"
330         # 7DRL additions: GOD_FAVOR
331         string = str(world_db["TURN"]) + "\n" + \
332                  str(world_db["GOD_FAVOR"]) + "\n" + \
333                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
334                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
335                  inventory + "%\n" + \
336                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
337                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
338                  str(world_db["MAP_LENGTH"]) + "\n"
339         length = world_db["MAP_LENGTH"]
340
341         fov = bytearray(b' ' * (length ** 2))
342         ord_v = ord("v")
343         for pos in [pos for pos in range(length ** 2)
344                         if ord_v == world_db["Things"][0]["fovmap"][pos]]:
345             fov[pos] = world_db["MAP"][pos]
346         for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
347                       for id in world_db["Things"]
348                       if not world_db["Things"][id]["carried"]
349                       if world_db["Things"][id]["T_TYPE"] == tid
350                       if world_db["Things"][0]["fovmap"][
351                            world_db["Things"][id]["T_POSY"] * length
352                            + world_db["Things"][id]["T_POSX"]] == ord_v]:
353             type = world_db["Things"][id]["T_TYPE"]
354             c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
355             fov[world_db["Things"][id]["T_POSY"] * length
356                 + world_db["Things"][id]["T_POSX"]] = c
357         string = write_map(string, fov)
358
359         mem = world_db["Things"][0]["T_MEMMAP"][:]
360         for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
361                       for mt in world_db["Things"][0]["T_MEMTHING"]
362                       if mt[0] == tid]:
363              c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
364              mem[(mt[1] * length) + mt[2]] = ord(c)
365         string = write_map(string, mem)
366
367         metamapA = bytearray(b'0' * (length ** 2))  # #
368         for id in [id for id in world_db["Things"]  # #
369                       if not world_db["Things"][id]["carried"]  # #
370                       if world_db["Things"][id]["T_LIFEPOINTS"]  # #
371                       if world_db["Things"][0]["fovmap"][  # #
372                            world_db["Things"][id]["T_POSY"] * length  # #
373                            + world_db["Things"][id]["T_POSX"]] == ord_v]:  # #
374             pos = (world_db["Things"][id]["T_POSY"] * length  # #
375                   + world_db["Things"][id]["T_POSX"])  # #
376             if id == 0 or world_db["EMPATHY"]:  # #
377                 type = world_db["Things"][id]["T_TYPE"]  # #
378                 max_hp = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]  # #
379                 third_of_hp = max_hp / 3  # #
380                 hp = world_db["Things"][id]["T_LIFEPOINTS"]  # #
381                 add = 0  # #
382                 if hp > 2 * third_of_hp:  # #
383                      add = 2  # #
384                 elif hp > third_of_hp:  # #
385                     add = 1  # #
386                 metamapA[pos] = ord('a') + add # #
387             else:  # #
388                 metamapA[pos] = ord('X')  # #
389         for mt in world_db["Things"][0]["T_MEMTHING"]:  # #
390             pos = mt[1] * length + mt[2]  # #
391             if metamapA[pos] < ord('2'):  # #
392                 metamapA[pos] += 1  # #
393         string = write_map(string, metamapA)  # #
394         
395         metamapB = bytearray(b' ' * (length ** 2))  # #
396         for id in [id for id in world_db["Things"]  # #
397                       if not world_db["Things"][id]["carried"]  # #
398                       if world_db["Things"][id]["T_LIFEPOINTS"]  # #
399                       if world_db["Things"][0]["fovmap"][  # #
400                            world_db["Things"][id]["T_POSY"] * length  # #
401                            + world_db["Things"][id]["T_POSX"]] == ord_v]:  # #
402             pos = (world_db["Things"][id]["T_POSY"] * length  # #
403                   + world_db["Things"][id]["T_POSX"])  # #
404             if id == 0 or world_db["EMPATHY"]:  # #
405                 action = world_db["Things"][id]["T_COMMAND"]  # #
406                 if 0 != action:  # #
407                     name = world_db["ThingActions"][action]["TA_NAME"]  # #
408                 else:  # #
409                     name = " "  # #
410                 metamapB[pos] = ord(name[0]) # #
411         string = write_map(string, metamapB)  # #
412
413         atomic_write(io_db["path_worldstate"], string, delete=False)
414         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
415         io_db["worldstate_updateable"] = False
416
417
418 def replay_game():
419     """Replay game from record file.
420
421     Use opts.replay as breakpoint turn to which to replay automatically before
422     switching to manual input by non-meta commands in server input file
423     triggering further reads of record file. Ensure opts.replay is at least 1.
424     Run try_worldstate_update() before each interactive obey()/read_command().
425     """
426     if opts.replay < 1:
427         opts.replay = 1
428     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
429           " (if so late a turn is to be found).")
430     if not os.access(io_db["path_record"], os.F_OK):
431         raise SystemExit("No record file found to replay.")
432     io_db["file_record"] = open(io_db["path_record"], "r")
433     io_db["file_record"].prefix = "record file line "
434     io_db["file_record"].line_n = 1
435     while world_db["TURN"] < opts.replay:
436         line = io_db["file_record"].readline()
437         if "" == line:
438             break
439         obey(line.rstrip(), io_db["file_record"].prefix
440              + str(io_db["file_record"].line_n))
441         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
442     while True:
443         try_worldstate_update()
444         obey(read_command(), "in file", replay=True)
445
446
447 def play_game():
448     """Play game by server input file commands. Before, load save file found.
449
450     If no save file is found, a new world is generated from the commands in the
451     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
452     command and all that follow via the server input file. Run
453     try_worldstate_update() before each interactive obey()/read_command().
454     """
455     if os.access(io_db["path_save"], os.F_OK):
456         obey_lines_in_file(io_db["path_save"], "save")
457     else:
458         if not os.access(io_db["path_worldconf"], os.F_OK):
459             msg = "No world config file from which to start a new world."
460             raise SystemExit(msg)
461         obey_lines_in_file(io_db["path_worldconf"], "world config ",
462                            do_record=True)
463         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
464     while True:
465         try_worldstate_update()
466         obey(read_command(), "in file", do_record=True)
467
468
469 def make_map():
470     """(Re-)make island map.
471
472     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
473     start with one land cell in the middle, then go into cycle of repeatedly
474     selecting a random sea cell and transforming it into land if it is neighbor
475     to land. The cycle ends when a land cell is due to be created at the map's
476     border. Then put some trees on the map (TODO: more precise algorithm desc).
477     """
478     # 7DRL: Also add some ":" cells, and (not surrounded by trees!) "_" altar.
479
480     def is_neighbor(coordinates, type):
481         y = coordinates[0]
482         x = coordinates[1]
483         length = world_db["MAP_LENGTH"]
484         ind = y % 2
485         diag_west = x + (ind > 0)
486         diag_east = x + (ind < (length - 1))
487         pos = (y * length) + x
488         if (y > 0 and diag_east
489             and type == chr(world_db["MAP"][pos - length + ind])) \
490            or (x < (length - 1)
491                and type == chr(world_db["MAP"][pos + 1])) \
492            or (y < (length - 1) and diag_east
493                and type == chr(world_db["MAP"][pos + length + ind])) \
494            or (y > 0 and diag_west
495                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
496            or (x > 0
497                and type == chr(world_db["MAP"][pos - 1])) \
498            or (y < (length - 1) and diag_west
499                and type == chr(world_db["MAP"][pos + length - (not ind)])):
500             return True
501         return False
502
503     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
504     length = world_db["MAP_LENGTH"]
505     add_half_width = (not (length % 2)) * int(length / 2)
506     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
507     while (1):
508         y = rand.next() % length
509         x = rand.next() % length
510         pos = (y * length) + x
511         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
512             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
513                 break
514             world_db["MAP"][pos] = ord(".")
515     n_trees = int((length ** 2) / 16)
516     i_trees = 0
517     while (i_trees <= n_trees):
518         single_allowed = rand.next() % 32
519         y = rand.next() % length
520         x = rand.next() % length
521         pos = (y * length) + x
522         if "." == chr(world_db["MAP"][pos]) \
523           and ((not single_allowed) or is_neighbor((y, x), "X")):
524             world_db["MAP"][pos] = ord("X")
525             i_trees += 1
526     # This all-too-precise replica of the original C code misses iter_limit().
527     n_colons = int((length ** 2) / 16)  # #
528     i_colons = 0  # #
529     while (i_colons <= n_colons):  # #
530         single_allowed = rand.next() % 256  # #
531         y = rand.next() % length  # #
532         x = rand.next() % length  # #
533         pos = (y * length) + x  # #
534         if ("." == chr(world_db["MAP"][pos])  # #
535           and ((not single_allowed) or is_neighbor((y, x), ":"))):  # #
536             world_db["MAP"][pos] = ord(":")  # #
537             i_colons += 1  # #
538     altar_placed = False  # #
539     while not altar_placed:  # #
540         y = rand.next() % length  # #
541         x = rand.next() % length  # #
542         pos = (y * length) + x  # #
543         if (("." == chr(world_db["MAP"][pos]  # #
544              or ":" == chr(world_db["MAP"][pos]))
545             and not is_neighbor((y, x), "X"))):  # #
546             world_db["MAP"][pos] = ord("_")  # #
547             world_db["altar"] = (y, x)  # #
548             altar_placed = True  # #
549
550
551 def eat_vs_hunger_threshold(thingtype):
552     """Return satiation cost of eating for type. Good food for it must be >."""
553     hunger_unit = hunger_per_turn(thingtype)
554     actiontype = [id for id in world_db["ThingActions"]
555                if world_db["ThingActions"][id]["TA_NAME"] == "use"][0]
556     return world_db["ThingActions"][actiontype]["TA_EFFORT"] * hunger_unit
557
558
559 def update_map_memory(t, age_map=True):
560     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
561
562     def age_some_memdepthmap_on_nonfov_cells():
563         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
564         # ord_v = ord("v")
565         # ord_0 = ord("0")
566         # ord_9 = ord("9")
567         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
568         #             if not ord_v == t["fovmap"][pos]
569         #             if ord_0 <= t["T_MEMDEPTHMAP"][pos]
570         #             if ord_9 > t["T_MEMDEPTHMAP"][pos]
571         #             if not rand.next() % (2 **
572         #                                   (t["T_MEMDEPTHMAP"][pos] - 48))]:
573         #     t["T_MEMDEPTHMAP"][pos] += 1
574         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
575         fovmap = c_pointer_to_bytearray(t["fovmap"])
576         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
577
578     def update_mem_and_memdepthmap_via_fovmap():
579         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
580         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
581         #             if ord_v == t["fovmap"][pos]]:
582         #     t["T_MEMDEPTHMAP"][pos] = ord_0
583         #     t["T_MEMMAP"][pos] = world_db["MAP"][pos]
584         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
585         memmap = c_pointer_to_bytearray(t["T_MEMMAP"])
586         fovmap = c_pointer_to_bytearray(t["fovmap"])
587         map = c_pointer_to_bytearray(world_db["MAP"])
588         libpr.update_mem_and_memdepthmap_via_fovmap(map, fovmap, memdepthmap,
589                                                     memmap)
590
591     if not t["T_MEMMAP"]:
592         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
593     if not t["T_MEMDEPTHMAP"]:
594         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
595     update_mem_and_memdepthmap_via_fovmap()
596     if age_map:
597         age_some_memdepthmap_on_nonfov_cells()
598     ord_v = ord("v")
599     t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
600                        if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
601                                                + mt[2]]]
602     maplength = world_db["MAP_LENGTH"]
603     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])  # #
604     [t["T_MEMTHING"].append((world_db["Things"][id]["T_TYPE"],
605                              world_db["Things"][id]["T_POSY"],
606                              world_db["Things"][id]["T_POSX"]))
607      for id in world_db["Things"]
608      if not world_db["Things"][id]["carried"]
609      if not world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
610                                                               ["TT_LIFEPOINTS"]
611      if (t == world_db["Things"][0] or
612          (world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
613                                                           ["TT_TOOL"] == "food"
614           and 
615           world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
616                                                     ["TT_TOOLPOWER"] > eat_cost
617          )
618         )
619      if ord_v == t["fovmap"][(world_db["Things"][id]["T_POSY"] * maplength)
620                                        + world_db["Things"][id]["T_POSX"]]]
621
622
623 def set_world_inactive():
624     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
625     server_test()
626     if os.access(io_db["path_worldstate"], os.F_OK):
627         os.remove(io_db["path_worldstate"])
628     world_db["WORLD_ACTIVE"] = 0
629
630
631 def integer_test(val_string, min, max=None):
632     """Return val_string if possible integer >= min and <= max, else None."""
633     try:
634         val = int(val_string)
635         if val < min or (max is not None and val > max):
636             raise ValueError
637         return val
638     except ValueError:
639         msg = "Ignoring: Please use integer >= " + str(min)
640         if max is not None:
641             msg += " and <= " + str(max)
642         msg += "."
643         print(msg)
644         return None
645
646
647 def setter(category, key, min, max=None):
648     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
649     if category is None:
650         def f(val_string):
651             val = integer_test(val_string, min, max)
652             if None != val:
653                 world_db[key] = val
654     else:
655         if category == "Thing":
656             id_store = command_tid
657             decorator = test_Thing_id
658         elif category == "ThingType":
659             id_store = command_ttid
660             decorator = test_ThingType_id
661         elif category == "ThingAction":
662             id_store = command_taid
663             decorator = test_ThingAction_id
664
665         @decorator
666         def f(val_string):
667             val = integer_test(val_string, min, max)
668             if None != val:
669                 world_db[category + "s"][id_store.id][key] = val
670     return f
671
672
673 def build_fov_map(t):
674     """Build Thing's FOV map."""
675     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
676     fovmap = c_pointer_to_bytearray(t["fovmap"])
677     map = c_pointer_to_bytearray(world_db["MAP"])
678     if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
679         raise RuntimeError("Malloc error in build_fov_Map().")
680
681
682 def log_help():
683     """Send quick usage info to log."""
684     log("Use 'w'/'e'/'s'/'d'/'x'/'c' to move, and 'w' to wait.")
685     log("Use 'p' to pick up objects, and 'D' to drop them.")
686     log("Some objects can be used (such as: eaten) by 'u' if they are in "
687         + "your inventory. Use 'Up'/'Down' to navigate the inventory.")
688     log("Use 'l' to toggle 'look' mode (move an exploration cursor instead of "
689         + "the player over the map).")
690     log("Use 'PgUp'/PgDn' to scroll the 'Things here' window.")
691     log("The stats window: T=TURN, H=HEALTH, S=SATIATION, G=GOD'S FAVOR.")
692     log("See README file for more details.")
693
694
695 def decrement_lifepoints(t):
696     """Decrement t's lifepoints by 1, and if to zero, corpse it.
697
698     If t is the player avatar, only blank its fovmap, so that the client may
699     still display memory data. On non-player things, erase fovmap and memory.
700     Dying actors drop all their things.
701     """
702     # 7DRL: Return TT_LIFEPOINTS if death, else 0.
703     # 7DRL: Re-spawn died-out species.
704     t["T_LIFEPOINTS"] -= 1
705     live_type = t["T_TYPE"] # 7DRL
706     if 0 == t["T_LIFEPOINTS"]:
707         for id in t["T_CARRIES"]:
708             t["T_CARRIES"].remove(id)
709             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
710             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
711             world_db["Things"][id]["carried"] = False
712         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
713         if world_db["Things"][0] == t:
714             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
715             log("You die.")
716             log("See README on how to start over.")
717         else:
718             t["fovmap"] = False
719             t["T_MEMMAP"] = False
720             t["T_MEMDEPTHMAP"] = False
721             t["T_MEMTHING"] = []
722             n_species = len([id for id in world_db["Things"]  # #
723                              if world_db["Things"][id]["T_TYPE"] == live_type])
724             if 0 == n_species:  # #
725                 if world_db["FAVOR_STAGE"] >= 3 and \
726                     live_type == world_db["ANIMAL_0"]:
727                     world_db["GOD_FAVOR"] += 3000
728                     log("Congratulations! The "
729                         + world_db["ThingTypes"][live_type]["TT_NAME"]
730                         + " species has died out. The Island God is pleased.")
731                 else:
732                     id = id_setter(-1, "Things")
733                     world_db["Things"][id] = new_Thing(live_type,
734                                                        world_db["altar"])
735                     log("The "
736                         + world_db["ThingTypes"][live_type]["TT_NAME"]
737                         + " species has temporarily died out. "
738                         + "One new-born is spawned at the altar.")
739         return world_db["ThingTypes"][live_type]["TT_LIFEPOINTS"]  # #
740     return 0  # #
741
742
743 def mv_yx_in_dir_legal(dir, y, x):
744     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
745     dir_c = dir.encode("ascii")[0]
746     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
747     if -1 == test:
748         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
749     return (test, libpr.result_y(), libpr.result_x())
750
751
752 def enter_altar():  # #
753     """What happens when the player enters the altar."""
754     if world_db["FAVOR_STAGE"] > 9000:
755        log("You step on a soul-less slab of stone.")
756        return
757     log("YOU ENTER SACRED GROUND.")
758     if world_db["FAVOR_STAGE"] == 0:
759         world_db["FAVOR_STAGE"] = 1
760         log("The Island God speaks to you: \"I don't trust you. You intrude "
761              + "on the island's affairs. I think you're a nuisance at best, "
762              + "and a danger to my children at worst. I will give you a "
763              + "chance to lighten my mood, however: For a while now, I've "
764              + "been trying to spread the plant "
765              + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"] + " (\""
766              + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_SYMBOL"]
767              + "\"). I have not been very successful so far. Maybe you can "
768              + "make yourself useful there. I will count each further "
769              + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"]
770              + " that grows to your favor.\"")
771     elif world_db["FAVOR_STAGE"] == 1 and world_db["GOD_FAVOR"] >= 100:
772         world_db["FAVOR_STAGE"] = 2
773         log("The Island God speaks to you: \"You could have done worse so "
774             + "far. Maybe you are not the worst to happen to this island "
775             + "since the metal birds threw the great lightning ball. Maybe "
776             + "you can help me spread another plant. It multiplies faster, "
777             + "and it is highly nutritious: "
778             + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"] + " (\""
779             + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_SYMBOL"]
780             + "\"). It is new. I give you the only example. Be very careful "
781             + "with it! I also give you another tool that may be helpful.\"")
782         id = id_setter(-1, "Things")
783         world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
784                                            world_db["altar"])
785         id = id_setter(-1, "Things")
786         world_db["Things"][id] = new_Thing(world_db["TOOL_0"],
787                                            world_db["altar"])
788     elif world_db["FAVOR_STAGE"] == 2 and \
789         0 == len([id for id in world_db["Things"]
790                   if world_db["Things"][id]["T_TYPE"]
791                      == world_db["PLANT_1"]]):
792         log("The Island God speaks to you: \"I am greatly disappointed that "
793             + "you lost all "
794             + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"]
795             + " this island had. Here is another one. It cost me great work. "
796            + "Be more careful this time when planting it.\"")
797         id = id_setter(-1, "Things")
798         world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
799                                            world_db["altar"])
800         world_db["GOD_FAVOR"] -= 250
801     elif world_db["FAVOR_STAGE"] == 2 and world_db["GOD_FAVOR"] >= 500:
802         world_db["FAVOR_STAGE"] = 3
803         log("The Island God speaks to you: \"The "
804             + world_db["ThingTypes"][world_db["ANIMAL_0"]]["TT_NAME"]
805             + " has lately become a pest. These creatures do not please me "
806             + "as much as they used to do. Exterminate them all. I will count "
807             + "each kill to your favor. To help you with the hunting, I grant "
808             + "you the empathy and knowledge to read animals.\"")
809         log("You will now see animals' health bars, and activities (\"m\": "
810             + "moving (maybe for an attack), \"u\": eating, \"p\": picking "
811             + "something up; no letter: waiting).")
812         world_db["EMPATHY"] = 1
813     elif world_db["FAVOR_STAGE"] == 3 and world_db["GOD_FAVOR"] >= 5000:
814         world_db["FAVOR_STAGE"] = 4
815         log("The Island God speaks to you: \"You know what animal I find the "
816              + "cutest? The "
817              + world_db["ThingTypes"][world_db["ANIMAL_1"]]["TT_NAME"]
818              + "! I think what this islands clearly needs more of is " 
819              + world_db["ThingTypes"][world_db["ANIMAL_1"]]["TT_NAME"]
820              + "s. Why don't you help? Support thm. Make sure they are well, "
821              + "and they will multiply faster. From now on, I will "
822              + "count each new-born "
823              + world_db["ThingTypes"][world_db["ANIMAL_1"]]["TT_NAME"]
824              + " (not spawned by me due to undo an extinction event) "
825              + "greatly to your favor. To help you with the feeding, here is "
826              + "something to make the ground bear more consumables.")
827         id = id_setter(-1, "Things")
828         world_db["Things"][id] = new_Thing(world_db["TOOL_1"],
829                                            world_db["altar"])
830     elif world_db["GOD_FAVOR"] > 20000:
831         log("The Island God speaks to you: \"You have proven yourself worthy"
832              + " of my respect. You were a good citizen to the island, and "
833              + "sometimes a better steward to its inhabitants than me. The "
834              + "island shall miss you when you leave. But you have earned "
835              + "the right to do so. Take this "
836              + world_db["ThingTypes"][world_db["SLIPPERS"]]["TT_NAME"]
837              + " and USE it when you please. It will take you to where you "
838              + "came from. (But do feel free to stay here as long as you "
839              + "like.)\"")
840         id = id_setter(-1, "Things")
841         world_db["Things"][id] = new_Thing(world_db["SLIPPERS"],
842                                            world_db["altar"])
843
844
845 def actor_wait(t):
846     """Make t do nothing (but loudly, if player avatar)."""
847     if t == world_db["Things"][0]:
848         log("You wait.")
849
850
851 def actor_move(t):
852     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
853     # 7DRL: Player wounding (worse: killing) others will lower God's favor.
854     # 7DRL: Player entering the altar triggers enter_altar().
855     # 7DRL: Player with axe chops down trees.
856     passable = False
857     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
858                                      t["T_POSY"], t["T_POSX"])
859     if 1 == move_result[0]:
860         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
861         hitted = [id for id in world_db["Things"]
862                   if world_db["Things"][id] != t
863                   if world_db["Things"][id]["T_LIFEPOINTS"]
864                   if world_db["Things"][id]["T_POSY"] == move_result[1]
865                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
866         if len(hitted):
867             hit_id = hitted[0]
868             hitted_type = world_db["Things"][hit_id]["T_TYPE"]
869             if t == world_db["Things"][0]:
870                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
871                 log("You wound " + hitted_name + ".")
872                 world_db["GOD_FAVOR"] -= 1  # #
873             elif 0 == hit_id:
874                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
875                 log(hitter_name +" wounds you.")
876             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
877             if test and world_db["FAVOR_STAGE"] >= 3 and \
878                hitted_type == world_db["ANIMAL_0"]:  # #
879                 world_db["GOD_FAVOR"] += 125
880             elif test and t == world_db["Things"][0]:  # #
881                 world_db["GOD_FAVOR"] -= 2 * test  # #
882             return
883         if (ord("X") == world_db["MAP"][pos]  # #
884             or ord("|") == world_db["MAP"][pos]):  # #
885             carries_axe = False  # #
886             for id in t["T_CARRIES"]:  # # 
887                 type = world_db["Things"][id]["T_TYPE"]  # #
888                 if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":  # #
889                     carries_axe = True  # #
890                     break  # #
891             if carries_axe:  # #
892                 axe_name = world_db["ThingTypes"][type]["TT_NAME"]  # #
893                 if t == world_db["Things"][0]:  # #
894                     log("With your " + axe_name + ", you chop!\n")  # #
895                     if ord("X") == world_db["MAP"][pos]:  # #
896                         world_db["GOD_FAVOR"] = -1  # #
897                 chop_power = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
898                 case_X = world_db["MAP"][pos] == ord("X")  # #
899                 if (chop_power > 0  # #
900                     and ((case_X and  # #
901                           0 == int(rand.next() / chop_power))  # #
902                     or (not case_X and  # #
903                              0 == int(rand.next() / (3 * chop_power))))):  # #
904                     if t == world_db["Things"][0]:  # #
905                         log("You chop it down.")  # #
906                     if world_db["MAP"][pos] == ord("X"):  # #
907                         world_db["GOD_FAVOR"] -= 10  # #
908                     world_db["MAP"][pos] = ord(".")   # #
909                     i = 3 if case_X else 1  # #
910                     for i in range(i):  # #
911                         id = id_setter(-1, "Things")  # #
912                         world_db["Things"][id] = \
913                           new_Thing(world_db["LUMBER"],  # #
914                                     (move_result[1], move_result[2]))  # # 
915                     build_fov_map(t)  # #
916                 return   # #
917         passable = ("." == chr(world_db["MAP"][pos]) or
918                     ":" == chr(world_db["MAP"][pos]) or # #
919                     "_" == chr(world_db["MAP"][pos])) # #
920     dir = [dir for dir in directions_db
921            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
922     if passable:
923         t["T_POSY"] = move_result[1]
924         t["T_POSX"] = move_result[2]
925         for id in t["T_CARRIES"]:
926             world_db["Things"][id]["T_POSY"] = move_result[1]
927             world_db["Things"][id]["T_POSX"] = move_result[2]
928         build_fov_map(t)
929         if t == world_db["Things"][0]:
930             log("You move " + dir + ".")
931             if (move_result[1] == world_db["altar"][0] and  # #
932                 move_result[2] == world_db["altar"][1]):  # #
933                 enter_altar()  # #
934     elif t == world_db["Things"][0]:
935         log("You fail to move " + dir + ".")
936
937
938 def actor_pick_up(t):
939     """Make t pick up (topmost?) Thing from ground into inventory.
940
941     Define topmostness by how low the thing's type ID is.
942     """
943     # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
944     # 7DRL: Non-players pick up nothing but food of good value to them.
945     used_slots = len(t["T_CARRIES"]) # #
946     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
947         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
948                if not world_db["Things"][id]["carried"]
949                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
950                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
951         if len(ids):
952             lowest_tid = -1
953             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"]) # #
954             for iid in ids:
955                 tid = world_db["Things"][iid]["T_TYPE"] 
956                 if lowest_tid == -1 or tid < lowest_tid:
957                     if (t != world_db["Things"][0] and  # #
958                         (world_db["ThingTypes"][tid]["TT_TOOL"] != "food"  # #
959                          or (world_db["ThingTypes"][tid]["TT_TOOLPOWER"]  # #
960                              <= eat_cost))):  # #
961                         continue  # #
962                     id = iid
963                     lowest_tid = tid
964             world_db["Things"][id]["carried"] = True
965             type = world_db["Things"][id]["T_TYPE"]  # #
966             if (t != world_db["Things"][0] # #
967                 and world_db["Things"][id]["T_PLAYERDROP"]  # #
968                 and world_db["ThingTypes"][type]["TT_TOOL"] == "food"):  # #
969                 score = world_db["ThingTypes"][type]["TT_TOOLPOWER"] / 32  # #
970                 world_db["GOD_FAVOR"] += score  # #
971                 world_db["Things"][id]["T_PLAYERDROP"] = 0  # #
972             t["T_CARRIES"].append(id)
973             if t == world_db["Things"][0]:
974                 log("You pick up an object.")
975         elif t == world_db["Things"][0]:
976             log("You try to pick up an object, but there is none.")
977     elif t == world_db["Things"][0]: # #
978         log("Can't pick up object: No storage room to carry more.") # #
979
980
981 def actor_drop(t):
982     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
983     # TODO: Handle case where T_ARGUMENT matches nothing.
984     if len(t["T_CARRIES"]):
985         id = t["T_CARRIES"][t["T_ARGUMENT"]]
986         t["T_CARRIES"].remove(id)
987         world_db["Things"][id]["carried"] = False
988         if t == world_db["Things"][0]:
989             log("You drop an object.")
990             world_db["Things"][id]["T_PLAYERDROP"] = 1  # #
991     elif t == world_db["Things"][0]:
992        log("You try to drop an object, but you own none.")
993
994
995 def actor_use(t):
996     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
997     # TODO: Handle case where T_ARGUMENT matches nothing.
998     # 7DLR: Handle SLIPPERS-type Thing use.
999     # 7DRL: Player with fertilizer fertilizes
1000     if len(t["T_CARRIES"]):
1001         id = t["T_CARRIES"][t["T_ARGUMENT"]]
1002         type = world_db["Things"][id]["T_TYPE"]
1003         if type == world_db["SLIPPERS"]:  # #
1004             if t == world_db["Things"][0]:  # #
1005                 log("You use the " + world_db["ThingTypes"][type]["TT_NAME"]  # #
1006                     + ". It glows in wondrous colors, and emits a sound as "  # #
1007                     + "if from a dying cat. The Island God laughs.\n")  # #
1008             t["T_LIFEPOINTS"] = 1  # #
1009             decrement_lifepoints(t)  # #
1010         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "axe"  # #
1011               and t == world_db["Things"][0]):  # #
1012                 log("To use this item for chopping, move towards a tree while "
1013                      + "carrying it in your inventory.")  # #
1014         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "carpentry"):  # #
1015             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1016             if (world_db["MAP"][pos] == ord("X")  # #
1017                 or world_db["MAP"][pos] == ord("|")):  # #
1018                 log("Can't build when standing on barrier.")  # #
1019                 return
1020             for id in [id for id in world_db["Things"]
1021                        if not world_db["Things"][id] == t
1022                        if not world_db["Things"][id]["carried"]
1023                        if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1024                        if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]:
1025                  log("Can't build when standing objects.")  # #
1026                  return
1027             for id in t["T_CARRIES"]:  # #
1028                 type_tool = world_db["Things"][id]["T_TYPE"]  # #
1029                 if (world_db["ThingTypes"][type_tool]["TT_TOOL"]  # #
1030                     == "carpentry"):  # #
1031                     break  # #
1032             wood_id = None  # #
1033             for id in t["T_CARRIES"]:  # #
1034                 type_material = world_db["Things"][id]["T_TYPE"]  # #
1035                 if (world_db["ThingTypes"][type_material]["TT_TOOL"]  # #
1036                     == "wood"):  # #
1037                     wood_id = id  # #
1038                     break  # #
1039             if wood_id != None:  # #
1040                 t["T_CARRIES"].remove(wood_id)  # #
1041                 del world_db["Things"][wood_id]  # #
1042                 world_db["MAP"][pos] = ord("|")  # #
1043                 log("With your "  # #
1044                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1045                     + " you build a wooden barrier from your "  # #
1046                     + world_db["ThingTypes"][type_material]["TT_NAME"]  # #
1047                     + ".")  # #
1048             else:  # #
1049                 log("You can't use a "  # #
1050                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1051                     + " without some wood in your inventory.")  # #
1052         elif world_db["ThingTypes"][type]["TT_TOOL"] == "fertilizer":  # #
1053             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1054             if world_db["MAP"][pos] == ord("."):
1055                 log("You create soil.")
1056                 world_db["MAP"][pos] = ord(":")
1057             else:
1058                 log("Can only make soil out of non-soil earth.")
1059         elif world_db["ThingTypes"][type]["TT_TOOL"] == "food":
1060             t["T_CARRIES"].remove(id)
1061             del world_db["Things"][id]
1062             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1063             if t == world_db["Things"][0]:
1064                 log("You consume this object.")
1065         elif t == world_db["Things"][0]:
1066             log("You try to use this object, but fail.")
1067     elif t == world_db["Things"][0]:
1068         log("You try to use an object, but you own none.")
1069
1070
1071 def thingproliferation(t, prol_map):
1072     """To chance of 1/TT_PROLIFERATE, create t offspring in open neighbor cell.
1073
1074     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
1075     marked "." in prol_map. If there are several map cell candidates, one is
1076     selected randomly.
1077     """
1078     # 7DRL: success increments God's mood
1079     # 7DRL: Plants (no TT_LIFEPOINTS) proliferate only on ":" ground.
1080     # 7DRL: Animals only proliferate when >= 0.9 their max HP.
1081     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
1082     if prolscore and \
1083       (world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] == 0 or  # #
1084        t["T_LIFEPOINTS"] >= 0.9 *  # #
1085                         world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]) \
1086        and \
1087       (1 == prolscore or 1 == (rand.next() % prolscore)):
1088         candidates = []
1089         for dir in [directions_db[key] for key in directions_db]:
1090             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
1091             pos = mv_result[1] * world_db["MAP_LENGTH"] + mv_result[2]
1092             if mv_result[0] and \
1093                (ord(":") == prol_map[pos]  # #
1094                 or (world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
1095                     and ord(".") == prol_map[pos])):
1096             # if mv_result[0] and ord(".") == prol_map[mv_result[1]
1097             #                                          * world_db["MAP_LENGTH"]
1098             #                                          + mv_result[2]]:
1099                 candidates.append((mv_result[1], mv_result[2]))
1100         if len(candidates):
1101             i = rand.next() % len(candidates)
1102             id = id_setter(-1, "Things")
1103             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
1104             world_db["Things"][id] = newT
1105             if (world_db["FAVOR_STAGE"] > 0  # #
1106                 and t["T_TYPE"] == world_db["PLANT_0"]):  # #
1107                 world_db["GOD_FAVOR"] += 5  # #
1108             elif t["T_TYPE"] == world_db["PLANT_1"]:  # #
1109                 world_db["GOD_FAVOR"] += 25  # #
1110             elif world_db["FAVOR_STAGE"] >= 4 and \
1111                  t["T_TYPE"] == world_db["ANIMAL_1"]:
1112                 log("The Island God smiles upon a new-born bear baby.")
1113                 world_db["GOD_FAVOR"] += 750
1114
1115
1116 def try_healing(t):
1117     """If t's HP < max, increment them if well-nourished, maybe waiting."""
1118     # 7DRL: Successful heals increment God's mood.
1119     if t["T_LIFEPOINTS"] < \
1120        world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
1121         wait_id = [id for id in world_db["ThingActions"]
1122                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1123         wait_divider = 8 if t["T_COMMAND"] == wait_id else 1
1124         testval = int(abs(t["T_SATIATION"]) / wait_divider)
1125         if (testval <= 1 or 1 == (rand.next() % testval)):
1126             t["T_LIFEPOINTS"] += 1
1127             if t == world_db["Things"][0]:
1128                 log("You heal.")
1129
1130
1131 def hunger_per_turn(type_id):
1132     """The amount of satiation score lost per turn for things of given type."""
1133     return int(math.sqrt(world_db["ThingTypes"][type_id]["TT_LIFEPOINTS"]))
1134
1135
1136 def hunger(t):
1137     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
1138     if t["T_SATIATION"] > -32768:
1139         t["T_SATIATION"] -= hunger_per_turn(t["T_TYPE"])
1140     if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
1141         if t == world_db["Things"][0]:
1142             if t["T_SATIATION"] < 0:
1143                 log("You suffer from hunger.")
1144             else:
1145                 log("You suffer from over-eating.")
1146         decrement_lifepoints(t)
1147
1148
1149 def get_dir_to_target(t, filter):
1150     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
1151
1152     The path-wise nearest target is chosen, via the shortest available path.
1153     Target must not be t. On succcess, return positive value, else False.
1154     Filters:
1155     "a": Thing in FOV is animate, but of ThingType, starts out weaker than t
1156          is, and its corpse would be healthy food for t
1157     "f": move away from an enemy – any visible actor whose thing type has more
1158          TT_LIFEPOINTS than t LIFEPOINTS, and might find t's corpse healthy
1159          food – if it is closer than n steps, where n will shrink as t's hunger
1160          grows; if enemy is too close, move towards (attack) the enemy instead;
1161          if no fleeing is possible, nor attacking useful, wait; don't tread on
1162          non-enemies for fleeing
1163     "c": Thing in memorized map is consumable of sufficient nutrition for t
1164     "s": memory map cell with greatest-reachable degree of unexploredness
1165     """
1166
1167     def zero_score_map_where_char_on_memdepthmap(c):
1168         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1169         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1170         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
1171         #     set_map_score(i, 0)
1172         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
1173         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
1174             raise RuntimeError("No score map allocated for "
1175                                "zero_score_map_where_char_on_memdepthmap().")
1176
1177     def set_map_score_at_thingpos(id, score):
1178         pos = world_db["Things"][id]["T_POSY"] * world_db["MAP_LENGTH"] \
1179                                      + world_db["Things"][id]["T_POSX"]
1180         set_map_score(pos, score)
1181
1182     def set_map_score(pos, score):
1183         test = libpr.set_map_score(pos, score)
1184         if test:
1185             raise RuntimeError("No score map allocated for set_map_score().")
1186
1187     def get_map_score(pos):
1188         result = libpr.get_map_score(pos)
1189         if result < 0:
1190             raise RuntimeError("No score map allocated for get_map_score().")
1191         return result
1192
1193     def animate_in_fov(Thing, maplength): # maplength needed for optimization?
1194         if not Thing["T_LIFEPOINTS"] or Thing["carried"] or Thing == t:
1195             return False
1196         pos = Thing["T_POSY"] * maplength + Thing["T_POSX"]
1197         if 118 == t["fovmap"][pos]: # optimization: 118 = ord("v")
1198             return True
1199
1200     def good_attack_target(v):
1201         eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1202         type = world_db["ThingTypes"][v["T_TYPE"]]
1203         type_corpse = world_db["ThingTypes"][type["TT_CORPSE_ID"]]
1204         if t["T_LIFEPOINTS"] > type["TT_LIFEPOINTS"] \
1205         and type_corpse["TT_TOOL"] == "food" \
1206         and type_corpse["TT_TOOLPOWER"] > eat_cost:
1207             return True
1208         return False
1209
1210     def good_flee_target(m):
1211         own_corpse_id = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
1212         corpse_type = world_db["ThingTypes"][own_corpse_id]
1213         targetness = 0 if corpse_type["TT_TOOL"] != "food" \
1214                        else corpse_type["TT_TOOLPOWER"]
1215         type = world_db["ThingTypes"][m["T_TYPE"]]
1216         if t["T_LIFEPOINTS"] < type["TT_LIFEPOINTS"] \
1217         and targetness > eat_vs_hunger_threshold(m["T_TYPE"]):
1218             return True
1219         return False
1220
1221     def seeing_thing():
1222         maplength = world_db["MAP_LENGTH"]
1223         if t["fovmap"] and "a" == filter:
1224             for id in world_db["Things"]:
1225                 if animate_in_fov(world_db["Things"][id], maplength):
1226                     if good_attack_target(world_db["Things"][id]):
1227                         return True
1228         elif t["fovmap"] and "f" == filter:
1229             for id in world_db["Things"]:
1230                 if animate_in_fov(world_db["Things"][id], maplength):
1231                     if good_flee_target(world_db["Things"][id]):
1232                         return True
1233         elif t["T_MEMMAP"] and "c" == filter:
1234             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1235             ord_blank = ord(" ")
1236             for mt in t["T_MEMTHING"]:
1237                 if ord_blank != t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
1238                                             + mt[2]] and \
1239                    (t != world_db["Things"][0] or \
1240                     (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"
1241                       and world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"]
1242                        > eat_cost)):
1243                     return True
1244         return False
1245
1246     def set_cells_passable_on_memmap_to_65534_on_scoremap():
1247         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1248         # ord_dot = ord(".")
1249         # memmap = t["T_MEMMAP"]
1250         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1251         #            if ord_dot == memmap[i]]:
1252         #     set_map_score(i, 65534) # i.e. 65535-1
1253         map = c_pointer_to_bytearray(t["T_MEMMAP"])
1254         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
1255             raise RuntimeError("No score map allocated for "
1256                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
1257
1258     def init_score_map():
1259         test = libpr.init_score_map()
1260         if test:
1261             raise RuntimeError("Malloc error in init_score_map().")
1262         set_cells_passable_on_memmap_to_65534_on_scoremap()
1263         maplength = world_db["MAP_LENGTH"]
1264         if "a" == filter:
1265             [set_map_score_at_thingpos(id, 0)
1266              for id in world_db["Things"]
1267              if animate_in_fov(world_db["Things"][id], maplength)
1268              if good_attack_target(world_db["Things"][id])]
1269         elif "f" == filter:
1270             [set_map_score_at_thingpos(id, 0)
1271              for id in world_db["Things"]
1272              if animate_in_fov(world_db["Things"][id], maplength)
1273              if good_flee_target(world_db["Things"][id])]
1274         elif "c" == filter:
1275             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1276             ord_blank = ord(" ")
1277             [set_map_score(mt[1] * maplength + mt[2], 0)
1278              for mt in t["T_MEMTHING"]
1279              if ord_blank != t["T_MEMMAP"][mt[1] * maplength + mt[2]]
1280              if t != world_db["Things"][0] or
1281                 (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food" and
1282                  world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"] > eat_cost)]
1283         elif "s" == filter:
1284             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
1285         if "f" == filter:
1286             [set_map_score_at_thingpos(id, 65535)
1287              for id in world_db["Things"]
1288              if animate_in_fov(world_db["Things"][id], maplength)
1289              if get_map_score(world_db["Things"][id]["T_POSY"] * maplength
1290                               + world_db["Things"][id]["T_POSX"])]
1291         elif "a" != filter:
1292             [set_map_score_at_thingpos(id, 65535)
1293              for id in world_db["Things"]
1294              if animate_in_fov(world_db["Things"][id], maplength)]
1295
1296     def rand_target_dir(neighbors, cmp, dirs):
1297         candidates = []
1298         n_candidates = 0
1299         for i in range(len(dirs)):
1300             if cmp == neighbors[i]:
1301                 candidates.append(dirs[i])
1302                 n_candidates += 1
1303         return candidates[rand.next() % n_candidates] if n_candidates else 0
1304
1305     def get_neighbor_scores(dirs, eye_pos):
1306         scores = []
1307         if libpr.ready_neighbor_scores(eye_pos):
1308             raise RuntimeError("No score map allocated for " +
1309                                "ready_neighbor_scores.()")
1310         for i in range(len(dirs)):
1311             scores.append(libpr.get_neighbor_score(i))
1312         return scores
1313
1314     def get_dir_from_neighbors():
1315         dir_to_target = False
1316         dirs = "edcxsw"
1317         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1318         neighbors = get_neighbor_scores(dirs, eye_pos)
1319         minmax_start = 0 if "f" == filter else 65535 - 1
1320         minmax_neighbor = minmax_start
1321         for i in range(len(dirs)):
1322             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
1323                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1324                or ("f" != filter and minmax_neighbor > neighbors[i]):
1325                 minmax_neighbor = neighbors[i]
1326         if minmax_neighbor != minmax_start:
1327             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1328         if "f" == filter:
1329             distance = get_map_score(eye_pos)
1330             fear_distance = world_db["MAP_LENGTH"]
1331             if t["T_SATIATION"] < 0 and math.sqrt(-t["T_SATIATION"]) > 0:
1332                 fear_distance = fear_distance / math.sqrt(-t["T_SATIATION"])
1333             attack_distance = 1
1334             if not dir_to_target:
1335                 if attack_distance >= distance:
1336                     dir_to_target = rand_target_dir(neighbors,
1337                                                     distance - 1, dirs)
1338                 elif fear_distance >= distance:
1339                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1340                                       if
1341                                       world_db["ThingActions"][id]["TA_NAME"]
1342                                          == "wait"][0]
1343                     return 1
1344             elif dir_to_target and fear_distance < distance:
1345                 dir_to_target = 0
1346         return dir_to_target
1347
1348     dir_to_target = False
1349     mem_depth_c = b' '
1350     run_i = 9 + 1 if "s" == filter else 1
1351     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1352         run_i -= 1
1353         init_score_map()
1354         mem_depth_c = b'9' if b' ' == mem_depth_c \
1355                            else bytes([mem_depth_c[0] - 1])
1356         if libpr.dijkstra_map():
1357             raise RuntimeError("No score map allocated for dijkstra_map().")
1358         dir_to_target = get_dir_from_neighbors()
1359         libpr.free_score_map()
1360         if dir_to_target and str == type(dir_to_target):
1361             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1362                               if world_db["ThingActions"][id]["TA_NAME"]
1363                                  == "move"][0]
1364             t["T_ARGUMENT"] = ord(dir_to_target)
1365     return dir_to_target
1366
1367
1368 def standing_on_food(t):
1369     """Return True/False whether t is standing on healthy consumable."""
1370     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1371     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1372                if not world_db["Things"][id]["carried"]
1373                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1374                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1375                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1376                   ["TT_TOOL"] == "food"
1377                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1378                   ["TT_TOOLPOWER"] > eat_cost]:
1379         return True
1380     return False
1381
1382
1383 def get_inventory_slot_to_consume(t):
1384     """Return invent. slot of healthiest consumable(if any healthy),else -1."""
1385     cmp_food = -1
1386     selection = -1
1387     i = 0
1388     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1389     for id in t["T_CARRIES"]:
1390         type = world_db["Things"][id]["T_TYPE"]
1391         if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
1392            and world_db["ThingTypes"][type]["TT_TOOLPOWER"]:
1393             nutvalue = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1394             tmp_cmp = abs(t["T_SATIATION"] + nutvalue - eat_cost)
1395             if (cmp_food < 0 and tmp_cmp < abs(t["T_SATIATION"])) \
1396             or tmp_cmp < cmp_food:
1397                 cmp_food = tmp_cmp
1398                 selection = i
1399         i += 1
1400     return selection
1401
1402
1403 def ai(t):
1404     """Determine next command/argment for actor t via AI algorithms."""
1405     # 7DRL add: Don't pick up or search things when inventory is full.
1406     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1407                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1408     if get_dir_to_target(t, "f"):
1409         return
1410     sel = get_inventory_slot_to_consume(t)
1411     if -1 != sel:
1412         t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1413                           if world_db["ThingActions"][id]["TA_NAME"]
1414                              == "use"][0]
1415         t["T_ARGUMENT"] = sel
1416     elif standing_on_food(t):
1417         if  (len(t["T_CARRIES"]) <  # #
1418              world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]):  # #
1419             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1420                                  if world_db["ThingActions"][id]["TA_NAME"]
1421                                  == "pick_up"][0]
1422     else:
1423         going_to_known_food_spot = get_dir_to_target(t, "c")
1424         if not going_to_known_food_spot:
1425             aiming_for_walking_food = get_dir_to_target(t, "a")
1426             if not aiming_for_walking_food:
1427                 get_dir_to_target(t, "s")
1428
1429
1430 def turn_over():
1431     """Run game world and its inhabitants until new player input expected."""
1432     # 7DRL: effort of move action is TA_EFFORT / sqrt(TT_LIFEPOINTS)
1433     id = 0
1434     whilebreaker = False
1435     while world_db["Things"][0]["T_LIFEPOINTS"]:
1436         proliferable_map = world_db["MAP"][:]
1437         for id in [id for id in world_db["Things"]
1438                    if not world_db["Things"][id]["carried"]]:
1439             y = world_db["Things"][id]["T_POSY"]
1440             x = world_db["Things"][id]["T_POSX"]
1441             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord("X")
1442         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1443             if not id in world_db["Things"] or \
1444                world_db["Things"][id]["carried"]:   # May have been consumed or
1445                 continue                            # picked up during turn …
1446             Thing = world_db["Things"][id]
1447             if Thing["T_LIFEPOINTS"]:
1448                 if not Thing["T_COMMAND"]:
1449                     update_map_memory(Thing)
1450                     if 0 == id:
1451                         whilebreaker = True
1452                         break
1453                     ai(Thing)
1454                 try_healing(Thing)
1455                 hunger(Thing)
1456                 if Thing["T_LIFEPOINTS"]:
1457                     Thing["T_PROGRESS"] += 1
1458                     taid = [a for a in world_db["ThingActions"]
1459                               if a == Thing["T_COMMAND"]][0]
1460                     ThingAction = world_db["ThingActions"][taid]
1461                     #if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1462                     effort = ThingAction["TA_EFFORT"]  # #
1463                     if ThingAction["TA_NAME"] == "move":  # #
1464                         type = Thing["T_TYPE"]  # #
1465                         max_hp = (world_db["ThingTypes"][type]  # #
1466                                  ["TT_LIFEPOINTS"])  # #
1467                         effort = int(effort / math.sqrt(max_hp))  # #
1468                     if Thing["T_PROGRESS"] == effort:  # #
1469                         eval("actor_" + ThingAction["TA_NAME"])(Thing)
1470                         Thing["T_COMMAND"] = 0
1471                         Thing["T_PROGRESS"] = 0
1472             thingproliferation(Thing, proliferable_map)
1473         if whilebreaker:
1474             break
1475         world_db["TURN"] += 1
1476
1477
1478 def new_Thing(type, pos=(0, 0)):
1479     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1480     thing = {
1481         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1482         "T_ARGUMENT": 0,
1483         "T_PROGRESS": 0,
1484         "T_SATIATION": 0,
1485         "T_COMMAND": 0,
1486         "T_PLAYERDROP": 0,  # #
1487         "T_TYPE": type,
1488         "T_POSY": pos[0],
1489         "T_POSX": pos[1],
1490         "T_CARRIES": [],
1491         "carried": False,
1492         "T_MEMTHING": [],
1493         "T_MEMMAP": False,
1494         "T_MEMDEPTHMAP": False,
1495         "fovmap": False
1496     }
1497     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1498         build_fov_map(thing)
1499     return thing
1500
1501
1502 def id_setter(id, category, id_store=False, start_at_1=False):
1503     """Set ID of object of category to manipulate ID unused? Create new one.
1504
1505     The ID is stored as id_store.id (if id_store is set). If the integer of the
1506     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1507     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1508     always returned when no new object is created, otherwise the new object's
1509     ID.
1510     """
1511     min = 0 if start_at_1 else -1
1512     if str == type(id):
1513         id = integer_test(id, min)
1514     if None != id:
1515         if id in world_db[category]:
1516             if id_store:
1517                 id_store.id = id
1518             return None
1519         else:
1520             if (start_at_1 and 0 == id) \
1521                or ((not start_at_1) and (id < 0)):
1522                 id = 0 if start_at_1 else -1
1523                 while 1:
1524                     id = id + 1
1525                     if id not in world_db[category]:
1526                         break
1527             if id_store:
1528                 id_store.id = id
1529     return id
1530
1531
1532 def command_ping():
1533     """Send PONG line to server output file."""
1534     strong_write(io_db["file_out"], "PONG\n")
1535
1536
1537 def command_quit():
1538     """Abort server process."""
1539     if None == opts.replay:
1540         if world_db["WORLD_ACTIVE"]:
1541             save_world()
1542         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1543     raise SystemExit("received QUIT command")
1544
1545
1546 def command_thingshere(str_y, str_x):
1547     """Write to out file list of Things known to player at coordinate y, x."""
1548     # 7DRL: terrain, too
1549     if world_db["WORLD_ACTIVE"]:
1550         y = integer_test(str_y, 0, 255)
1551         x = integer_test(str_x, 0, 255)
1552         length = world_db["MAP_LENGTH"]
1553         if None != y and None != x and y < length and x < length:
1554             pos = (y * world_db["MAP_LENGTH"]) + x
1555             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1556             pos = y * world_db["MAP_LENGTH"] + x;  # #
1557             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1558                 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1559                               for id in world_db["Things"]
1560                               if not world_db["Things"][id]["carried"]
1561                               if world_db["Things"][id]["T_TYPE"] == tid
1562                               if y == world_db["Things"][id]["T_POSY"]
1563                               if x == world_db["Things"][id]["T_POSX"]]:
1564                     type = world_db["Things"][id]["T_TYPE"]
1565                     name = world_db["ThingTypes"][type]["TT_NAME"]
1566                     strong_write(io_db["file_out"], name + "\n")
1567             else:
1568                 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1569                               for mt in world_db["Things"][0]["T_MEMTHING"]
1570                               if mt[0] == tid if y == mt[1] if x == mt[2]]:
1571                     name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1572                     strong_write(io_db["file_out"], name + "\n")
1573             if world_db["Things"][0]["T_MEMMAP"][pos] == ord("~"):  # #
1574                 name = "(terrain: SEA)"  # #
1575             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("."):  # #
1576                 name = "(terrain: EARTH)"  # #
1577             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord(":"):  # #
1578                 name = "(terrain: SOIL)"  # #
1579             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("X"):  # #
1580                 name = "(terrain: TREE)"  # #
1581             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("|"):  # #
1582                 name = "(terrain: WALL)"  # #
1583             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("_"):  # #
1584                 name = "(terrain: ALTAR)"  # #
1585             else:  # #
1586                 name = "(?)"  # #
1587             strong_write(io_db["file_out"], name + "\n")  # #
1588             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1589         else:
1590             print("Ignoring: Invalid map coordinates.")
1591     else:
1592         print("Ignoring: Command only works on existing worlds.")
1593
1594
1595 def play_commander(action, args=False):
1596     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1597
1598     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1599     """
1600
1601     def set_command():
1602
1603         if action == "drop":
1604             t = world_db["Things"][0]
1605             if 0 == len(t["T_CARRIES"]):
1606                 log("You have nothing to drop in your inventory.")
1607                 return
1608
1609         elif action == "pick_up":
1610             t = world_db["Things"][0]
1611             ids = [id for id in world_db["Things"] if id
1612                    if not world_db["Things"][id]["carried"]
1613                    if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1614                    if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
1615             if not len(ids):
1616                 log("No object to pick up.")
1617                 return
1618             used_slots = len(t["T_CARRIES"]) # #
1619             if used_slots >= world_db["ThingTypes"][t["T_TYPE"]] \
1620                 ["TT_STORAGE"]: # #
1621                 log("Can't pick up: No storage room to carry anything more.")
1622                 return
1623
1624         elif action == "move":
1625             t = world_db["Things"][0]
1626             passable = False
1627             move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
1628                                              t["T_POSY"], t["T_POSX"])
1629             if 1 == move_result[0]:
1630                 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
1631                 if ord("~") == world_db["MAP"][pos]:
1632                     log("You can't swim.")
1633                     return
1634                 if (ord("X") == world_db["MAP"][pos]  # #
1635                     or ord("|") == world_db["MAP"][pos]):  # #
1636                     carries_axe = False  # #
1637                     for id in t["T_CARRIES"]:  # #
1638                         type = world_db["Things"][id]["T_TYPE"]  # #
1639                         if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":  # #
1640                             carries_axe = True  # #
1641                             break  # #
1642                     if not carries_axe:  # #
1643                         log("You can't move there.")
1644                         return
1645             else:
1646                 log("You can't move there.")
1647                 return
1648
1649         elif action == "use":
1650             t = world_db["Things"][0]
1651             if len(t["T_CARRIES"]):
1652                 id = t["T_CARRIES"][t["T_ARGUMENT"]]
1653                 type = world_db["Things"][id]["T_TYPE"]
1654                 if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":
1655                     log("To use this item for chopping, move towards a tree "
1656                          + "while carrying it in your inventory.")
1657                     return
1658                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "carpentry":
1659                     pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1660                     if (world_db["MAP"][pos] == ord("X")  # #
1661                         or world_db["MAP"][pos] == ord("|")):  # #
1662                         log("Can't build when standing on barrier.")  # #
1663                         return
1664                     for id in [id for id in world_db["Things"]
1665                                if not world_db["Things"][id] == t
1666                                if not world_db["Things"][id]["carried"]
1667                                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1668                                if world_db["Things"][id]["T_POSX"] ==t["T_POSX"]]:
1669                          log("Can't build when standing objects.")  # #
1670                          return
1671                     for id in t["T_CARRIES"]:  # #
1672                         type_tool = world_db["Things"][id]["T_TYPE"]  # #
1673                         if (world_db["ThingTypes"][type_tool]["TT_TOOL"]  # #
1674                             == "carpentry"):  # #
1675                             break  # #
1676                     wood_id = None  # #
1677                     for id in t["T_CARRIES"]:  # #
1678                         type_material = world_db["Things"][id]["T_TYPE"]  # #
1679                         if (world_db["ThingTypes"][type_material]["TT_TOOL"]  # #
1680                             == "wood"):  # #
1681                             wood_id = id  # #
1682                             break  # #
1683                     if wood_id == None:  # #
1684                         log("You can't use a "  # #
1685                             + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1686                             + " without some wood in your inventory.")  # #
1687                         return
1688                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "fertilizer":  # #
1689                     pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1690                     if not world_db["MAP"][pos] == ord("."):
1691                         log("Can only make soil out of non-soil earth.")
1692                         return
1693                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "wood":  # #
1694                         log("To use wood, you need a carpentry tool.")  # #
1695                         return
1696             else:
1697                 log("You have nothing to use in your inventory.")
1698                 return
1699
1700
1701         id = [x for x in world_db["ThingActions"]
1702                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1703         world_db["Things"][0]["T_COMMAND"] = id
1704         turn_over()
1705
1706     def set_command_and_argument_int(str_arg):
1707         val = integer_test(str_arg, 0, 255)
1708         if None != val:
1709             world_db["Things"][0]["T_ARGUMENT"] = val
1710             set_command()
1711
1712     def set_command_and_argument_movestring(str_arg):
1713         if str_arg in directions_db:
1714             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1715             set_command()
1716         else:
1717             print("Ignoring: Argument must be valid direction string.")
1718
1719     if action == "move":
1720         return set_command_and_argument_movestring
1721     elif args:
1722         return set_command_and_argument_int
1723     else:
1724         return set_command
1725
1726
1727 def command_seedrandomness(seed_string):
1728     """Set rand seed to int(seed_string)."""
1729     val = integer_test(seed_string, 0, 4294967295)
1730     if None != val:
1731         rand.seed = val
1732
1733
1734 def command_makeworld(seed_string):
1735     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1736
1737     Seed rand with seed. Do more only with a "wait" ThingAction and
1738     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1739     world_db["Things"] emptied, call make_map() and set
1740     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1741     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1742     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1743     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1744     Call log_help().
1745     """
1746
1747     # def free_pos(plant=False):
1748     def free_pos(plant=False):  # #
1749         i = 0
1750         while 1:
1751             err = "Space to put thing on too hard to find. Map too small?"
1752             while 1:
1753                 y = rand.next() % world_db["MAP_LENGTH"]
1754                 x = rand.next() % world_db["MAP_LENGTH"]
1755                 pos = y * world_db["MAP_LENGTH"] + x;
1756                 if (not plant  # #
1757                     and "." == chr(world_db["MAP"][pos])) \
1758                    or ":" == chr(world_db["MAP"][pos]):  # #
1759                     break
1760                 i += 1
1761                 if i == 65535:
1762                     raise SystemExit(err)
1763             # Replica of C code, wrongly ignores animatedness of new Thing.
1764             pos_clear = (0 == len([id for id in world_db["Things"]
1765                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1766                                    if world_db["Things"][id]["T_POSY"] == y
1767                                    if world_db["Things"][id]["T_POSX"] == x]))
1768             if pos_clear:
1769                 break
1770         return (y, x)
1771
1772     val = integer_test(seed_string, 0, 4294967295)
1773     if None == val:
1774         return
1775     rand.seed = val
1776     player_will_be_generated = False
1777     playertype = world_db["PLAYER_TYPE"]
1778     for ThingType in world_db["ThingTypes"]:
1779         if playertype == ThingType:
1780             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1781                 player_will_be_generated = True
1782             break
1783     if not player_will_be_generated:
1784         print("Ignoring: No player type with start number >0 defined.")
1785         return
1786     wait_action = False
1787     for ThingAction in world_db["ThingActions"]:
1788         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1789             wait_action = True
1790     if not wait_action:
1791         print("Ignoring: No thing action with name 'wait' defined.")
1792         return
1793     for name in specials:  # #
1794         if world_db[name] not in world_db["ThingTypes"]:  # #
1795             print("Ignoring: No valid " + name + " set.")  # #
1796             return  # #
1797     world_db["Things"] = {}
1798     make_map()
1799     world_db["WORLD_ACTIVE"] = 1
1800     world_db["TURN"] = 1
1801     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1802         id = id_setter(-1, "Things")
1803         world_db["Things"][id] = new_Thing(playertype, free_pos())
1804     if not world_db["Things"][0]["fovmap"]:
1805         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1806         world_db["Things"][0]["fovmap"] = empty_fovmap
1807     update_map_memory(world_db["Things"][0])
1808     for type in world_db["ThingTypes"]:
1809         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1810             if type != playertype:
1811                 id = id_setter(-1, "Things")
1812                 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"]  # #
1813                 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1814     strong_write(io_db["file_out"], "NEW_WORLD\n")
1815     log_help()
1816
1817
1818 def command_maplength(maplength_string):
1819     """Redefine map length. Invalidate map, therefore lose all things on it."""
1820     val = integer_test(maplength_string, 1, 256)
1821     if None != val:
1822         world_db["MAP_LENGTH"] = val
1823         world_db["MAP"] = False
1824         set_world_inactive()
1825         world_db["Things"] = {}
1826         libpr.set_maplength(val)
1827
1828
1829 def command_worldactive(worldactive_string):
1830     """Toggle world_db["WORLD_ACTIVE"] if possible.
1831
1832     An active world can always be set inactive. An inactive world can only be
1833     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1834     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1835     Also call log_help().
1836     """
1837     # 7DRL: altar must be on map, and specials must be set for active world.
1838     val = integer_test(worldactive_string, 0, 1)
1839     if None != val:
1840         if 0 != world_db["WORLD_ACTIVE"]:
1841             if 0 == val:
1842                 set_world_inactive()
1843             else:
1844                 print("World already active.")
1845         elif 0 == world_db["WORLD_ACTIVE"]:
1846             wait_exists = False
1847             for ThingAction in world_db["ThingActions"]:
1848                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1849                     wait_exists = True
1850                     break
1851             player_exists = False
1852             for Thing in world_db["Things"]:
1853                 if 0 == Thing:
1854                     player_exists = True
1855                     break
1856             specials_set = True  # #
1857             for name in specials:  # #
1858                 if world_db[name] not in world_db["ThingTypes"]:  # #
1859                     specials_set = False  # #
1860             altar_found = False  # #
1861             if world_db["MAP"]:  # #
1862                 pos = world_db["MAP"].find(b'_')  # #
1863                 if pos > 0:  # #
1864                     y = int(pos / world_db["MAP_LENGTH"])  # #
1865                     x = pos % world_db["MAP_LENGTH"]  # #
1866                     world_db["altar"] = (y, x)  # #
1867                     altar_found = True  # #
1868             if wait_exists and player_exists and world_db["MAP"] \
1869                and specials_set:  # #
1870                 for id in world_db["Things"]:
1871                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1872                         build_fov_map(world_db["Things"][id])
1873                         if 0 == id:
1874                             update_map_memory(world_db["Things"][id], False)
1875                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1876                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1877                     world_db["Things"][0]["fovmap"] = empty_fovmap
1878                 world_db["WORLD_ACTIVE"] = 1
1879                 log_help()
1880             else:
1881                 print("Ignoring: Not all conditions for world activation met.")
1882
1883
1884 def specialtypesetter(name):  # #
1885     """Setter world_db[name], deactivating world if set int no ThingType."""
1886     def helper(str_int):
1887         val = integer_test(str_int, 0)
1888         if None != val:
1889             world_db[name] = val
1890             if world_db["WORLD_ACTIVE"] \
1891                and world_db[name] not in world_db["ThingTypes"]:
1892                 world_db["WORLD_ACTIVE"] = 0
1893                 print(name + " fits no known ThingType, deactivating world.")
1894     return helper
1895
1896
1897 def test_for_id_maker(object, category):
1898     """Return decorator testing for object having "id" attribute."""
1899     def decorator(f):
1900         def helper(*args):
1901             if hasattr(object, "id"):
1902                 f(*args)
1903             else:
1904                 print("Ignoring: No " + category +
1905                       " defined to manipulate yet.")
1906         return helper
1907     return decorator
1908
1909
1910 def command_tid(id_string):
1911     """Set ID of Thing to manipulate. ID unused? Create new one.
1912
1913     Default new Thing's type to the first available ThingType, others: zero.
1914     """
1915     id = id_setter(id_string, "Things", command_tid)
1916     if None != id:
1917         if world_db["ThingTypes"] == {}:
1918             print("Ignoring: No ThingType to settle new Thing in.")
1919             return
1920         type = list(world_db["ThingTypes"].keys())[0]
1921         world_db["Things"][id] = new_Thing(type)
1922
1923
1924 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1925
1926
1927 @test_Thing_id
1928 def command_tcommand(str_int):
1929     """Set T_COMMAND of selected Thing."""
1930     val = integer_test(str_int, 0)
1931     if None != val:
1932         if 0 == val or val in world_db["ThingActions"]:
1933             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1934         else:
1935             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1936
1937
1938 @test_Thing_id
1939 def command_ttype(str_int):
1940     """Set T_TYPE of selected Thing."""
1941     val = integer_test(str_int, 0)
1942     if None != val:
1943         if val in world_db["ThingTypes"]:
1944             world_db["Things"][command_tid.id]["T_TYPE"] = val
1945         else:
1946             print("Ignoring: ThingType ID belongs to no known ThingType.")
1947
1948
1949 @test_Thing_id
1950 def command_tcarries(str_int):
1951     """Append int(str_int) to T_CARRIES of selected Thing.
1952
1953     The ID int(str_int) must not be of the selected Thing, and must belong to a
1954     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1955     """
1956     val = integer_test(str_int, 0)
1957     if None != val:
1958         if val == command_tid.id:
1959             print("Ignoring: Thing cannot carry itself.")
1960         elif val in world_db["Things"] \
1961              and not world_db["Things"][val]["carried"]:
1962             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1963             world_db["Things"][val]["carried"] = True
1964         else:
1965             print("Ignoring: Thing not available for carrying.")
1966     # Note that the whole carrying structure is different from the C version:
1967     # Carried-ness is marked by a "carried" flag, not by Things containing
1968     # Things internally.
1969
1970
1971 @test_Thing_id
1972 def command_tmemthing(str_t, str_y, str_x):
1973     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1974
1975     The type must fit to an existing ThingType, and the position into the map.
1976     """
1977     type = integer_test(str_t, 0)
1978     posy = integer_test(str_y, 0, 255)
1979     posx = integer_test(str_x, 0, 255)
1980     if None != type and None != posy and None != posx:
1981         if type not in world_db["ThingTypes"] \
1982            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1983             print("Ignoring: Illegal value for thing type or position.")
1984         else:
1985             memthing = (type, posy, posx)
1986             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1987
1988
1989 def setter_map(maptype):
1990     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1991
1992     If no map of maptype exists yet, initialize it with ' ' bytes first.
1993     """
1994
1995     def valid_map_line(str_int, mapline):
1996         val = integer_test(str_int, 0, 255)
1997         if None != val:
1998             if val >= world_db["MAP_LENGTH"]:
1999                 print("Illegal value for map line number.")
2000             elif len(mapline) != world_db["MAP_LENGTH"]:
2001                 print("Map line length is unequal map width.")
2002             else:
2003                 return val
2004         return None
2005
2006     def nonThingMap_helper(str_int, mapline):
2007         val = valid_map_line(str_int, mapline)
2008         if None != val:
2009             length = world_db["MAP_LENGTH"]
2010             if not world_db["MAP"]:
2011                 map = bytearray(b' ' * (length ** 2))
2012             else:
2013                 map = world_db["MAP"]
2014             map[val * length:(val * length) + length] = mapline.encode()
2015             if not world_db["MAP"]:
2016                 world_db["MAP"] = map
2017
2018     @test_Thing_id
2019     def ThingMap_helper(str_int, mapline):
2020         val = valid_map_line(str_int, mapline)
2021         if None != val:
2022             length = world_db["MAP_LENGTH"]
2023             if not world_db["Things"][command_tid.id][maptype]:
2024                 map = bytearray(b' ' * (length ** 2))
2025             else:
2026                 map = world_db["Things"][command_tid.id][maptype]
2027             map[val * length:(val * length) + length] = mapline.encode()
2028             if not world_db["Things"][command_tid.id][maptype]:
2029                 world_db["Things"][command_tid.id][maptype] = map
2030
2031     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
2032
2033
2034 def setter_tpos(axis):
2035     """Generate setter for T_POSX or  T_POSY of selected Thing.
2036
2037     If world is active, rebuilds animate things' fovmap, player's memory map.
2038     """
2039     @test_Thing_id
2040     def helper(str_int):
2041         val = integer_test(str_int, 0, 255)
2042         if None != val:
2043             if val < world_db["MAP_LENGTH"]:
2044                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
2045                 if world_db["WORLD_ACTIVE"] \
2046                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
2047                     build_fov_map(world_db["Things"][command_tid.id])
2048                     if 0 == command_tid.id:
2049                         update_map_memory(world_db["Things"][command_tid.id])
2050             else:
2051                 print("Ignoring: Position is outside of map.")
2052     return helper
2053
2054
2055 def command_ttid(id_string):
2056     """Set ID of ThingType to manipulate. ID unused? Create new one.
2057
2058     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
2059     "", others: 0.
2060     """
2061     id = id_setter(id_string, "ThingTypes", command_ttid)
2062     if None != id:
2063         world_db["ThingTypes"][id] = {
2064             "TT_NAME": "(none)",
2065             "TT_TOOLPOWER": 0,
2066             "TT_LIFEPOINTS": 0,
2067             "TT_PROLIFERATE": 0,
2068             "TT_START_NUMBER": 0,
2069             "TT_STORAGE": 0, # #
2070             "TT_SYMBOL": "?",
2071             "TT_CORPSE_ID": id,
2072             "TT_TOOL": ""
2073         }
2074
2075
2076 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
2077
2078
2079 @test_ThingType_id
2080 def command_ttname(name):
2081     """Set TT_NAME of selected ThingType."""
2082     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
2083
2084
2085 @test_ThingType_id
2086 def command_tttool(name):
2087     """Set TT_TOOL of selected ThingType."""
2088     world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
2089
2090
2091 @test_ThingType_id
2092 def command_ttsymbol(char):
2093     """Set TT_SYMBOL of selected ThingType. """
2094     if 1 == len(char):
2095         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
2096     else:
2097         print("Ignoring: Argument must be single character.")
2098
2099
2100 @test_ThingType_id
2101 def command_ttcorpseid(str_int):
2102     """Set TT_CORPSE_ID of selected ThingType."""
2103     val = integer_test(str_int, 0)
2104     if None != val:
2105         if val in world_db["ThingTypes"]:
2106             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
2107         else:
2108             print("Ignoring: Corpse ID belongs to no known ThignType.")
2109
2110
2111 def command_taid(id_string):
2112     """Set ID of ThingAction to manipulate. ID unused? Create new one.
2113
2114     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
2115     """
2116     id = id_setter(id_string, "ThingActions", command_taid, True)
2117     if None != id:
2118         world_db["ThingActions"][id] = {
2119             "TA_EFFORT": 1,
2120             "TA_NAME": "wait"
2121         }
2122
2123
2124 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
2125
2126
2127 @test_ThingAction_id
2128 def command_taname(name):
2129     """Set TA_NAME of selected ThingAction.
2130
2131     The name must match a valid thing action function. If after the name
2132     setting no ThingAction with name "wait" remains, call set_world_inactive().
2133     """
2134     if name == "wait" or name == "move" or name == "use" or name == "drop" \
2135        or name == "pick_up":
2136         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
2137         if 1 == world_db["WORLD_ACTIVE"]:
2138             wait_defined = False
2139             for id in world_db["ThingActions"]:
2140                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
2141                     wait_defined = True
2142                     break
2143             if not wait_defined:
2144                 set_world_inactive()
2145     else:
2146         print("Ignoring: Invalid action name.")
2147     # In contrast to the original,naming won't map a function to a ThingAction.
2148
2149
2150 def command_ai():
2151     """Call ai() on player Thing, then turn_over()."""
2152     ai(world_db["Things"][0])
2153     turn_over()
2154
2155
2156 """Commands database.
2157
2158 Map command start tokens to ([0]) number of expected command arguments, ([1])
2159 the command's meta-ness (i.e. is it to be written to the record file, is it to
2160 be ignored in replay mode if read from server input file), and ([2]) a function
2161 to be called on it.
2162 """
2163 commands_db = {
2164     "QUIT": (0, True, command_quit),
2165     "PING": (0, True, command_ping),
2166     "THINGS_HERE": (2, True, command_thingshere),
2167     "MAKE_WORLD": (1, False, command_makeworld),
2168     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
2169     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
2170     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
2171     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
2172     "MAP_LENGTH": (1, False, command_maplength),
2173     "WORLD_ACTIVE": (1, False, command_worldactive),
2174     "MAP": (2, False, setter_map("MAP")),
2175     "FAVOR_STAGE": (1, False, setter(None, "FAVOR_STAGE", 0, 65535)),  # #
2176     "SLIPPERS": (1, False, specialtypesetter("SLIPPERS")),  # #
2177     "PLANT_0": (1, False, specialtypesetter("PLANT_0")),  # #
2178     "PLANT_1": (1, False, specialtypesetter("PLANT_1")),  # #
2179     "LUMBER": (1, False, specialtypesetter("LUMBER")),  # #
2180     "TOOL_0": (1, False, specialtypesetter("TOOL_0")),  # #
2181     "TOOL_1": (1, False, specialtypesetter("TOOL_1")),  # #
2182     "ANIMAL_0": (1, False, specialtypesetter("ANIMAL_0")),  # #
2183     "ANIMAL_1": (1, False, specialtypesetter("ANIMAL_1")),  # #
2184     "EMPATHY": (1, False, setter(None, "EMPATHY", 0, 1)),  # #
2185     "TA_ID": (1, False, command_taid),
2186     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
2187     "TA_NAME": (1, False, command_taname),
2188     "TT_ID": (1, False, command_ttid),
2189     "TT_NAME": (1, False, command_ttname),
2190     "TT_TOOL": (1, False, command_tttool),
2191     "TT_SYMBOL": (1, False, command_ttsymbol),
2192     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
2193     "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
2194     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
2195                                          0, 255)),
2196     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
2197                                         0, 65535)),
2198     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
2199     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)),  # #
2200     "T_ID": (1, False, command_tid),
2201     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
2202     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
2203     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
2204     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
2205     "T_COMMAND": (1, False, command_tcommand),
2206     "T_TYPE": (1, False, command_ttype),
2207     "T_CARRIES": (1, False, command_tcarries),
2208     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
2209     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
2210     "T_MEMTHING": (3, False, command_tmemthing),
2211     "T_POSY": (1, False, setter_tpos("Y")),
2212     "T_POSX": (1, False, setter_tpos("X")),
2213     "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)),  # #
2214     "wait": (0, False, play_commander("wait")),
2215     "move": (1, False, play_commander("move")),
2216     "pick_up": (0, False, play_commander("pick_up")),
2217     "drop": (1, False, play_commander("drop", True)),
2218     "use": (1, False, play_commander("use", True)),
2219     "ai": (0, False, command_ai)
2220 }
2221 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
2222
2223
2224 """World state database. With sane default values. (Randomness is in rand.)"""
2225 world_db = {
2226     "TURN": 0,
2227     "MAP_LENGTH": 64,
2228     "PLAYER_TYPE": 0,
2229     "WORLD_ACTIVE": 0,
2230     "GOD_FAVOR": 0,  # #
2231     "MAP": False,
2232     "FAVOR_STAGE": 0,  # #
2233     "SLIPPERS": 0,  # #
2234     "PLANT_0": 0,  # #
2235     "PLANT_1": 0,  # #
2236     "ANIMAL_0": 0,  # #
2237     "ANIMAL_1": 0,  # #
2238     "LUMBER": 0,  # #
2239     "TOOL_0": 0,  # #
2240     "TOOL_1": 0,  # #
2241     "EMPATHY": 0,  # #
2242     "ThingActions": {},
2243     "ThingTypes": {},
2244     "Things": {}
2245 }
2246
2247 # 7DRL-specific!
2248 """Special type settings."""
2249 specials = ["SLIPPERS", "PLANT_0", "PLANT_1", "LUMBER", "TOOL_0", "TOOL_1",
2250             "ANIMAL_0", "ANIMAL_1"]
2251
2252 """Mapping of direction names to internal direction chars."""
2253 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
2254                  "west": "s", "north-west": "w", "north-east": "e"}
2255
2256 """File IO database."""
2257 io_db = {
2258     "path_save": "save",
2259     "path_record": "record_save",
2260     "path_worldconf": "confserver/world",
2261     "path_server": "server/",
2262     "path_in": "server/in",
2263     "path_out": "server/out",
2264     "path_worldstate": "server/worldstate",
2265     "tmp_suffix": "_tmp",
2266     "kicked_by_rival": False,
2267     "worldstate_updateable": False
2268 }
2269
2270
2271 try:
2272     libpr = prep_library()
2273     rand = RandomnessIO()
2274     opts = parse_command_line_arguments()
2275     if opts.savefile:
2276         io_db["path_save"] = opts.savefile
2277         io_db["path_record"] = "record_" + opts.savefile
2278     setup_server_io()
2279     if opts.verbose:
2280         io_db["verbose"] = True
2281     if None != opts.replay:
2282         replay_game()
2283     else:
2284         play_game()
2285 except SystemExit as exit:
2286     print("ABORTING: " + exit.args[0])
2287 except:
2288     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
2289     raise
2290 finally:
2291     cleanup_server_io()