home · contact · privacy
Add forgiving checks for player mistakes. (Lots of duplicate code here.)
[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("See README file for more details.")
692
693
694 def decrement_lifepoints(t):
695     """Decrement t's lifepoints by 1, and if to zero, corpse it.
696
697     If t is the player avatar, only blank its fovmap, so that the client may
698     still display memory data. On non-player things, erase fovmap and memory.
699     Dying actors drop all their things.
700     """
701     # 7DRL: Return 1 if death, else 0.
702     t["T_LIFEPOINTS"] -= 1
703     if 0 == t["T_LIFEPOINTS"]:
704         for id in t["T_CARRIES"]:
705             t["T_CARRIES"].remove(id)
706             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
707             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
708             world_db["Things"][id]["carried"] = False
709         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
710         if world_db["Things"][0] == t:
711             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
712             log("You die.")
713             log("See README on how to start over.")
714         else:
715             t["fovmap"] = False
716             t["T_MEMMAP"] = False
717             t["T_MEMDEPTHMAP"] = False
718             t["T_MEMTHING"] = []
719         return world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
720     return 0  # #
721
722
723 def mv_yx_in_dir_legal(dir, y, x):
724     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
725     dir_c = dir.encode("ascii")[0]
726     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
727     if -1 == test:
728         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
729     return (test, libpr.result_y(), libpr.result_x())
730
731
732 def enter_altar():  # #
733      """What happens when the player enters the altar."""
734      if world_db["FAVOR_STAGE"] > 9000:
735         log("You step on a soul-less slab of stone.")
736         return
737      log("YOU ENTER SACRED GROUND.")
738      if world_db["FAVOR_STAGE"] == 0:
739          world_db["FAVOR_STAGE"] = 1
740          log("The Island God speaks to you: \"I don't trust you. You intrude "
741               + "on the island's affairs. I think you're a nuisance at best, "
742               + "and a danger to my children at worst. I will give you a "
743               + "chance to lighten my mood, however: For a while now, I've "
744               + "been trying to spread the plant "
745               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"] + " (\""
746               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_SYMBOL"]
747               + "\"). I have not been very successful so far. Maybe you can "
748               + "make yourself useful there. I will count each further "
749               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"]
750               + " that grows to your favor.\"")
751      elif world_db["FAVOR_STAGE"] == 1 and world_db["GOD_FAVOR"] >= 100:
752          world_db["FAVOR_STAGE"] = 2
753          log("The Island God speaks to you: \"You could have done worse so "
754              + "far. Maybe you are not the worst to happen to this island "
755              + "since the metal birds threw the great lightning ball. Maybe "
756              + "you can help me spread another plant. It multiplies faster, "
757              + "and it is highly nutritious: "
758              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"] + " (\""
759              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_SYMBOL"]
760              + "\"). It is new. I give you the only example. Be very careful "
761              + "with it! I also give you another tool that may be helpful.\"")
762          id = id_setter(-1, "Things")
763          world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
764                                             world_db["altar"])
765          id = id_setter(-1, "Things")
766          world_db["Things"][id] = new_Thing(world_db["TOOL_0"],
767                                             world_db["altar"])
768      elif world_db["FAVOR_STAGE"] == 2 and \
769           0 == len([id for id in world_db["Things"]
770                    if world_db["Things"][id]["T_TYPE"]
771                       == world_db["PLANT_1"]]):
772          log("The Island God speaks to you: \"I am greatly disappointed that "
773              + "you lost all "
774              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"]
775              + " this island had. Here is another one. It cost me great work. "
776              + "Be more careful this time.\"")
777          id = id_setter(-1, "Things")
778          world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
779                                             world_db["altar"])
780          world_db["GOD_FAVOR"] -= 250
781      elif world_db["GOD_FAVOR"] > 9000:
782          world_db["FAVOR_STAGE"] = 9001
783          log("The Island God speaks to you: \"You have proven yourself worthy"
784               + " of my respect. You were a good citizen to the island, and "
785               + "sometimes a better steward to its inhabitants than me. The "
786               + "island shall miss you when you leave. But you have earned "
787               + "the right to do so. Take this "
788               + world_db["ThingTypes"][world_db["SLIPPERS"]]["TT_NAME"]
789               + " and USE it when you please. It will take you to where you "
790               + "came from. (But do feel free to stay here as long as you "
791               + "like.)\"")
792          id = id_setter(-1, "Things")
793          world_db["Things"][id] = new_Thing(world_db["SLIPPERS"],
794                                             world_db["altar"])
795
796
797 def actor_wait(t):
798     """Make t do nothing (but loudly, if player avatar)."""
799     if t == world_db["Things"][0]:
800         log("You wait.")
801
802
803 def actor_move(t):
804     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
805     # 7DRL: Player wounding (worse: killing) others will lower God's favor.
806     # 7DRL: Player entering the altar triggers enter_altar().
807     # 7DRL: Player with axe chops down trees.
808     passable = False
809     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
810                                      t["T_POSY"], t["T_POSX"])
811     if 1 == move_result[0]:
812         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
813         hitted = [id for id in world_db["Things"]
814                   if world_db["Things"][id] != t
815                   if world_db["Things"][id]["T_LIFEPOINTS"]
816                   if world_db["Things"][id]["T_POSY"] == move_result[1]
817                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
818         if len(hitted):
819             hit_id = hitted[0]
820             if t == world_db["Things"][0]:
821                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
822                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
823                 log("You wound " + hitted_name + ".")
824                 world_db["GOD_FAVOR"] -= -1  # #
825             elif 0 == hit_id:
826                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
827                 log(hitter_name +" wounds you.")
828             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
829             if test and t == world_db["Things"][0]:  # #
830                 world_db["GOD_FAVOR"] -test  # #
831             return
832         if (ord("X") == world_db["MAP"][pos]  # #
833             or ord("|") == world_db["MAP"][pos]):  # #
834             carries_axe = False  # #
835             for id in t["T_CARRIES"]:  # # 
836                 type = world_db["Things"][id]["T_TYPE"]  # #
837                 if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":  # #
838                     carries_axe = True  # #
839                     break  # #
840             if carries_axe:  # #
841                 axe_name = world_db["ThingTypes"][type]["TT_NAME"]  # #
842                 if t == world_db["Things"][0]:  # #
843                     log("With your " + axe_name + ", you chop!\n")  # #
844                     if ord("X") == world_db["MAP"][pos]:  # #
845                         world_db["GOD_FAVOR"] = -1  # #
846                 chop_power = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
847                 case_X = world_db["MAP"][pos] == ord("X")  # #
848                 if (chop_power > 0  # #
849                     and ((case_X and  # #
850                           0 == int(rand.next() / chop_power))  # #
851                     or (not case_X and  # #
852                              0 == int(rand.next() / (3 * chop_power))))):  # #
853                     if t == world_db["Things"][0]:  # #
854                         log("You chop it down.")  # #
855                     if world_db["MAP"][pos] == ord("X"):  # #
856                         world_db["GOD_FAVOR"] = -10  # #
857                     world_db["MAP"][pos] = ord(".")   # #
858                     i = 3 if case_X else 1  # #
859                     for i in range(i):  # #
860                         id = id_setter(-1, "Things")  # #
861                         world_db["Things"][id] = \
862                           new_Thing(world_db["LUMBER"],  # #
863                                     (move_result[1], move_result[2]))  # # 
864                     build_fov_map(t)  # #
865                 return   # #
866         passable = ("." == chr(world_db["MAP"][pos]) or
867                     ":" == chr(world_db["MAP"][pos]) or # #
868                     "_" == chr(world_db["MAP"][pos])) # #
869     dir = [dir for dir in directions_db
870            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
871     if passable:
872         t["T_POSY"] = move_result[1]
873         t["T_POSX"] = move_result[2]
874         for id in t["T_CARRIES"]:
875             world_db["Things"][id]["T_POSY"] = move_result[1]
876             world_db["Things"][id]["T_POSX"] = move_result[2]
877         build_fov_map(t)
878         if t == world_db["Things"][0]:
879             log("You move " + dir + ".")
880             if (move_result[1] == world_db["altar"][0] and  # #
881                 move_result[2] == world_db["altar"][1]):  # #
882                 enter_altar()  # #
883     elif t == world_db["Things"][0]:
884         log("You fail to move " + dir + ".")
885
886
887 def actor_pick_up(t):
888     """Make t pick up (topmost?) Thing from ground into inventory.
889
890     Define topmostness by how low the thing's type ID is.
891     """
892     # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
893     # 7DRL: Non-players pick up nothing but food of good value to them.
894     used_slots = len(t["T_CARRIES"]) # #
895     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
896         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
897                if not world_db["Things"][id]["carried"]
898                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
899                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
900         if len(ids):
901             lowest_tid = -1
902             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"]) # #
903             for iid in ids:
904                 tid = world_db["Things"][iid]["T_TYPE"] 
905                 if lowest_tid == -1 or tid < lowest_tid:
906                     if (t != world_db["Things"][0] and  # #
907                         (world_db["ThingTypes"][tid]["TT_TOOL"] != "food"  # #
908                          or (world_db["ThingTypes"][tid]["TT_TOOLPOWER"]  # #
909                              <= eat_cost))):  # #
910                         continue  # #
911                     id = iid
912                     lowest_tid = tid
913             world_db["Things"][id]["carried"] = True
914             type = world_db["Things"][id]["T_TYPE"]  # #
915             if (t != world_db["Things"][0] # #
916                 and world_db["Things"][id]["T_PLAYERDROP"]  # #
917                 and world_db["ThingTypes"][type]["TT_TOOL"] == "food"):  # #
918                 score = world_db["ThingTypes"][type]["TT_TOOLPOWER"] / 32  # #
919                 world_db["GOD_FAVOR"] += score  # #
920                 world_db["Things"][id]["T_PLAYERDROP"] = 0  # #
921             t["T_CARRIES"].append(id)
922             if t == world_db["Things"][0]:
923                 log("You pick up an object.")
924         elif t == world_db["Things"][0]:
925             log("You try to pick up an object, but there is none.")
926     elif t == world_db["Things"][0]: # #
927         log("Can't pick up object: No storage room to carry more.") # #
928
929
930 def actor_drop(t):
931     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
932     # TODO: Handle case where T_ARGUMENT matches nothing.
933     if len(t["T_CARRIES"]):
934         id = t["T_CARRIES"][t["T_ARGUMENT"]]
935         t["T_CARRIES"].remove(id)
936         world_db["Things"][id]["carried"] = False
937         if t == world_db["Things"][0]:
938             log("You drop an object.")
939             world_db["Things"][id]["T_PLAYERDROP"] = 1  # #
940     elif t == world_db["Things"][0]:
941        log("You try to drop an object, but you own none.")
942
943
944 def actor_use(t):
945     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
946     # TODO: Handle case where T_ARGUMENT matches nothing.
947     # 7DLR: Handle SLIPPERS-type Thing use.
948     # 7DRL: Player with fertilizer fertilizes
949     if len(t["T_CARRIES"]):
950         id = t["T_CARRIES"][t["T_ARGUMENT"]]
951         type = world_db["Things"][id]["T_TYPE"]
952         if type == world_db["SLIPPERS"]:  # #
953             if t == world_db["Things"][0]:  # #
954                 log("You use the " + world_db["ThingTypes"][type]["TT_NAME"]  # #
955                     + ". It glows in wondrous colors, and emits a sound as "  # #
956                     + "if from a dying cat. The Island God laughs.\n")  # #
957             t["T_LIFEPOINTS"] = 1  # #
958             decrement_lifepoints(t)  # #
959         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "axe"  # #
960               and t == world_db["Things"][0]):  # #
961                 log("To use this item for chopping, move towards a tree while "
962                      + "carrying it in your inventory.")  # #
963         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "carpentry"):  # #
964             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
965             if (world_db["MAP"][pos] == ord("X")  # #
966                 or world_db["MAP"][pos] == ord("|")):  # #
967                 log("Can't build when standing on barrier.")  # #
968                 return
969             for id in [id for id in world_db["Things"]
970                        if not world_db["Things"][id] == t
971                        if not world_db["Things"][id]["carried"]
972                        if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
973                        if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]:
974                  log("Can't build when standing objects.")  # #
975                  return
976             for id in t["T_CARRIES"]:  # #
977                 type_tool = world_db["Things"][id]["T_TYPE"]  # #
978                 if (world_db["ThingTypes"][type_tool]["TT_TOOL"]  # #
979                     == "carpentry"):  # #
980                     break  # #
981             wood_id = None  # #
982             for id in t["T_CARRIES"]:  # #
983                 type_material = world_db["Things"][id]["T_TYPE"]  # #
984                 if (world_db["ThingTypes"][type_material]["TT_TOOL"]  # #
985                     == "wood"):  # #
986                     wood_id = id  # #
987                     break  # #
988             if wood_id != None:  # #
989                 t["T_CARRIES"].remove(wood_id)  # #
990                 del world_db["Things"][wood_id]  # #
991                 world_db["MAP"][pos] = ord("|")  # #
992                 log("With your "  # #
993                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
994                     + " you build a wooden barrier from your "  # #
995                     + world_db["ThingTypes"][type_material]["TT_NAME"]  # #
996                     + ".")  # #
997             else:  # #
998                 log("You can't use a "  # #
999                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1000                     + " without some wood in your inventory.")  # #
1001         elif world_db["ThingTypes"][type]["TT_TOOL"] == "fertilizer":  # #
1002             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1003             if world_db["MAP"][pos] == ord("."):
1004                 log("You create soil.")
1005                 world_db["MAP"][pos] = ord(":")
1006             else:
1007                 log("Can only make soil out of non-soil earth.")
1008         elif world_db["ThingTypes"][type]["TT_TOOL"] == "food":
1009             t["T_CARRIES"].remove(id)
1010             del world_db["Things"][id]
1011             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1012             if t == world_db["Things"][0]:
1013                 log("You consume this object.")
1014         elif t == world_db["Things"][0]:
1015             log("You try to use this object, but fail.")
1016     elif t == world_db["Things"][0]:
1017         log("You try to use an object, but you own none.")
1018
1019
1020 def thingproliferation(t, prol_map):
1021     """To chance of 1/TT_PROLIFERATE, create t offspring in open neighbor cell.
1022
1023     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
1024     marked "." in prol_map. If there are several map cell candidates, one is
1025     selected randomly.
1026     """
1027     # 7DRL: success increments God's mood
1028     # 7DRL: Plants (no TT_LIFEPOINTS) proliferate only on ":" ground.
1029     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
1030     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
1031         candidates = []
1032         for dir in [directions_db[key] for key in directions_db]:
1033             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
1034             pos = mv_result[1] * world_db["MAP_LENGTH"] + mv_result[2]
1035             if mv_result[0] and \
1036                (ord(":") == prol_map[pos]  # #
1037                 or (world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
1038                     and ord(".") == prol_map[pos])):
1039             # if mv_result[0] and ord(".") == prol_map[mv_result[1]
1040             #                                          * world_db["MAP_LENGTH"]
1041             #                                          + mv_result[2]]:
1042                 candidates.append((mv_result[1], mv_result[2]))
1043         if len(candidates):
1044             i = rand.next() % len(candidates)
1045             id = id_setter(-1, "Things")
1046             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
1047             world_db["Things"][id] = newT
1048             if (world_db["FAVOR_STAGE"] > 0  # #
1049                 and t["T_TYPE"] == world_db["PLANT_0"]):  # #
1050                 world_db["GOD_FAVOR"] += 5  # #
1051             elif t["T_TYPE"] == world_db["PLANT_1"]:  # #
1052                 world_db["GOD_FAVOR"] += 25  # #
1053
1054
1055 def try_healing(t):
1056     """If t's HP < max, increment them if well-nourished, maybe waiting."""
1057     # 7DRL: Successful heals increment God's mood.
1058     if t["T_LIFEPOINTS"] < \
1059        world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
1060         wait_id = [id for id in world_db["ThingActions"]
1061                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1062         wait_divider = 8 if t["T_COMMAND"] == wait_id else 1
1063         testval = int(abs(t["T_SATIATION"]) / wait_divider)
1064         if (testval <= 1 or 1 == (rand.next() % testval)):
1065             t["T_LIFEPOINTS"] += 1
1066             if t == world_db["Things"][0]:
1067                 log("You heal.")
1068
1069
1070 def hunger_per_turn(type_id):
1071     """The amount of satiation score lost per turn for things of given type."""
1072     return int(math.sqrt(world_db["ThingTypes"][type_id]["TT_LIFEPOINTS"]))
1073
1074
1075 def hunger(t):
1076     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
1077     if t["T_SATIATION"] > -32768:
1078         #max_hp = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]
1079         t["T_SATIATION"] -= hunger_per_turn(t["T_TYPE"]) # int(math.sqrt(max_hp))
1080     if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
1081         if t == world_db["Things"][0]:
1082             if t["T_SATIATION"] < 0:
1083                 log("You suffer from hunger.")
1084             else:
1085                 log("You suffer from over-eating.")
1086         decrement_lifepoints(t)
1087
1088
1089 def get_dir_to_target(t, filter):
1090     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
1091
1092     The path-wise nearest target is chosen, via the shortest available path.
1093     Target must not be t. On succcess, return positive value, else False.
1094     Filters:
1095     "a": Thing in FOV is animate, but of ThingType, starts out weaker than t
1096          is, and its corpse would be healthy food for t
1097     "f": move away from an enemy – any visible actor whose thing type has more
1098          TT_LIFEPOINTS than t LIFEPOINTS, and might find t's corpse healthy
1099          food – if it is closer than n steps, where n will shrink as t's hunger
1100          grows; if enemy is too close, move towards (attack) the enemy instead;
1101          if no fleeing is possible, nor attacking useful, wait; don't tread on
1102          non-enemies for fleeing
1103     "c": Thing in memorized map is consumable of sufficient nutrition for t
1104     "s": memory map cell with greatest-reachable degree of unexploredness
1105     """
1106
1107     def zero_score_map_where_char_on_memdepthmap(c):
1108         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1109         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1110         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
1111         #     set_map_score(i, 0)
1112         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
1113         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
1114             raise RuntimeError("No score map allocated for "
1115                                "zero_score_map_where_char_on_memdepthmap().")
1116
1117     def set_map_score_at_thingpos(id, score):
1118         pos = world_db["Things"][id]["T_POSY"] * world_db["MAP_LENGTH"] \
1119                                      + world_db["Things"][id]["T_POSX"]
1120         set_map_score(pos, score)
1121
1122     def set_map_score(pos, score):
1123         test = libpr.set_map_score(pos, score)
1124         if test:
1125             raise RuntimeError("No score map allocated for set_map_score().")
1126
1127     def get_map_score(pos):
1128         result = libpr.get_map_score(pos)
1129         if result < 0:
1130             raise RuntimeError("No score map allocated for get_map_score().")
1131         return result
1132
1133     def animate_in_fov(Thing, maplength): # maplength needed for optimization?
1134         if not Thing["T_LIFEPOINTS"] or Thing["carried"] or Thing == t:
1135             return False
1136         pos = Thing["T_POSY"] * maplength + Thing["T_POSX"]
1137         if 118 == t["fovmap"][pos]: # optimization: 118 = ord("v")
1138             return True
1139
1140     def good_attack_target(v):
1141         eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1142         type = world_db["ThingTypes"][v["T_TYPE"]]
1143         type_corpse = world_db["ThingTypes"][type["TT_CORPSE_ID"]]
1144         if t["T_LIFEPOINTS"] > type["TT_LIFEPOINTS"] \
1145         and type_corpse["TT_TOOL"] == "food" \
1146         and type_corpse["TT_TOOLPOWER"] > eat_cost:
1147             return True
1148         return False
1149
1150     def good_flee_target(m):
1151         own_corpse_id = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
1152         corpse_type = world_db["ThingTypes"][own_corpse_id]
1153         targetness = 0 if corpse_type["TT_TOOL"] != "food" \
1154                        else corpse_type["TT_TOOLPOWER"]
1155         type = world_db["ThingTypes"][m["T_TYPE"]]
1156         if t["T_LIFEPOINTS"] < type["TT_LIFEPOINTS"] \
1157         and targetness > eat_vs_hunger_threshold(m["T_TYPE"]):
1158             return True
1159         return False
1160
1161     def seeing_thing():
1162         maplength = world_db["MAP_LENGTH"]
1163         if t["fovmap"] and "a" == filter:
1164             for id in world_db["Things"]:
1165                 if animate_in_fov(world_db["Things"][id], maplength):
1166                     if good_attack_target(world_db["Things"][id]):
1167                         return True
1168         elif t["fovmap"] and "f" == filter:
1169             for id in world_db["Things"]:
1170                 if animate_in_fov(world_db["Things"][id], maplength):
1171                     if good_flee_target(world_db["Things"][id]):
1172                         return True
1173         elif t["T_MEMMAP"] and "c" == filter:
1174             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1175             ord_blank = ord(" ")
1176             for mt in t["T_MEMTHING"]:
1177                 if ord_blank != t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
1178                                             + mt[2]] and \
1179                    (t != world_db["Things"][0] or \
1180                     (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"
1181                       and world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"]
1182                        > eat_cost)):
1183                     return True
1184         return False
1185
1186     def set_cells_passable_on_memmap_to_65534_on_scoremap():
1187         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1188         # ord_dot = ord(".")
1189         # memmap = t["T_MEMMAP"]
1190         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1191         #            if ord_dot == memmap[i]]:
1192         #     set_map_score(i, 65534) # i.e. 65535-1
1193         map = c_pointer_to_bytearray(t["T_MEMMAP"])
1194         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
1195             raise RuntimeError("No score map allocated for "
1196                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
1197
1198     def init_score_map():
1199         test = libpr.init_score_map()
1200         if test:
1201             raise RuntimeError("Malloc error in init_score_map().")
1202         set_cells_passable_on_memmap_to_65534_on_scoremap()
1203         maplength = world_db["MAP_LENGTH"]
1204         if "a" == filter:
1205             [set_map_score_at_thingpos(id, 0)
1206              for id in world_db["Things"]
1207              if animate_in_fov(world_db["Things"][id], maplength)
1208              if good_attack_target(world_db["Things"][id])]
1209         elif "f" == filter:
1210             [set_map_score_at_thingpos(id, 0)
1211              for id in world_db["Things"]
1212              if animate_in_fov(world_db["Things"][id], maplength)
1213              if good_flee_target(world_db["Things"][id])]
1214         elif "c" == filter:
1215             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1216             ord_blank = ord(" ")
1217             [set_map_score(mt[1] * maplength + mt[2], 0)
1218              for mt in t["T_MEMTHING"]
1219              if ord_blank != t["T_MEMMAP"][mt[1] * maplength + mt[2]]
1220              if t != world_db["Things"][0] or
1221                 (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food" and
1222                  world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"] > eat_cost)]
1223         elif "s" == filter:
1224             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
1225         if "f" == filter:
1226             [set_map_score_at_thingpos(id, 65535)
1227              for id in world_db["Things"]
1228              if animate_in_fov(world_db["Things"][id], maplength)
1229              if get_map_score(world_db["Things"][id]["T_POSY"] * maplength
1230                               + world_db["Things"][id]["T_POSX"])]
1231         elif "a" != filter:
1232             [set_map_score_at_thingpos(id, 65535)
1233              for id in world_db["Things"]
1234              if animate_in_fov(world_db["Things"][id], maplength)]
1235
1236     def rand_target_dir(neighbors, cmp, dirs):
1237         candidates = []
1238         n_candidates = 0
1239         for i in range(len(dirs)):
1240             if cmp == neighbors[i]:
1241                 candidates.append(dirs[i])
1242                 n_candidates += 1
1243         return candidates[rand.next() % n_candidates] if n_candidates else 0
1244
1245     def get_neighbor_scores(dirs, eye_pos):
1246         scores = []
1247         if libpr.ready_neighbor_scores(eye_pos):
1248             raise RuntimeError("No score map allocated for " +
1249                                "ready_neighbor_scores.()")
1250         for i in range(len(dirs)):
1251             scores.append(libpr.get_neighbor_score(i))
1252         return scores
1253
1254     def get_dir_from_neighbors():
1255         dir_to_target = False
1256         dirs = "edcxsw"
1257         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1258         neighbors = get_neighbor_scores(dirs, eye_pos)
1259         minmax_start = 0 if "f" == filter else 65535 - 1
1260         minmax_neighbor = minmax_start
1261         for i in range(len(dirs)):
1262             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
1263                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1264                or ("f" != filter and minmax_neighbor > neighbors[i]):
1265                 minmax_neighbor = neighbors[i]
1266         if minmax_neighbor != minmax_start:
1267             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1268         if "f" == filter:
1269             distance = get_map_score(eye_pos)
1270             fear_distance = world_db["MAP_LENGTH"]
1271             if t["T_SATIATION"] < 0 and math.sqrt(-t["T_SATIATION"]) > 0:
1272                 fear_distance = fear_distance / math.sqrt(-t["T_SATIATION"])
1273             attack_distance = 1
1274             if not dir_to_target:
1275                 if attack_distance >= distance:
1276                     dir_to_target = rand_target_dir(neighbors,
1277                                                     distance - 1, dirs)
1278                 elif fear_distance >= distance:
1279                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1280                                       if
1281                                       world_db["ThingActions"][id]["TA_NAME"]
1282                                          == "wait"][0]
1283                     return 1
1284             elif dir_to_target and fear_distance < distance:
1285                 dir_to_target = 0
1286         return dir_to_target
1287
1288     dir_to_target = False
1289     mem_depth_c = b' '
1290     run_i = 9 + 1 if "s" == filter else 1
1291     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1292         run_i -= 1
1293         init_score_map()
1294         mem_depth_c = b'9' if b' ' == mem_depth_c \
1295                            else bytes([mem_depth_c[0] - 1])
1296         if libpr.dijkstra_map():
1297             raise RuntimeError("No score map allocated for dijkstra_map().")
1298         dir_to_target = get_dir_from_neighbors()
1299         libpr.free_score_map()
1300         if dir_to_target and str == type(dir_to_target):
1301             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1302                               if world_db["ThingActions"][id]["TA_NAME"]
1303                                  == "move"][0]
1304             t["T_ARGUMENT"] = ord(dir_to_target)
1305     return dir_to_target
1306
1307
1308 def standing_on_food(t):
1309     """Return True/False whether t is standing on healthy consumable."""
1310     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1311     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1312                if not world_db["Things"][id]["carried"]
1313                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1314                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1315                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1316                   ["TT_TOOL"] == "food"
1317                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1318                   ["TT_TOOLPOWER"] > eat_cost]:
1319         return True
1320     return False
1321
1322
1323 def get_inventory_slot_to_consume(t):
1324     """Return invent. slot of healthiest consumable(if any healthy),else -1."""
1325     cmp_food = -1
1326     selection = -1
1327     i = 0
1328     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1329     for id in t["T_CARRIES"]:
1330         type = world_db["Things"][id]["T_TYPE"]
1331         if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
1332            and world_db["ThingTypes"][type]["TT_TOOLPOWER"]:
1333             nutvalue = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1334             tmp_cmp = abs(t["T_SATIATION"] + nutvalue - eat_cost)
1335             if (cmp_food < 0 and tmp_cmp < abs(t["T_SATIATION"])) \
1336             or tmp_cmp < cmp_food:
1337                 cmp_food = tmp_cmp
1338                 selection = i
1339         i += 1
1340     return selection
1341
1342
1343 def ai(t):
1344     """Determine next command/argment for actor t via AI algorithms."""
1345     # 7DRL add: Don't pick up or search things when inventory is full.
1346     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1347                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1348     if get_dir_to_target(t, "f"):
1349         return
1350     sel = get_inventory_slot_to_consume(t)
1351     if -1 != sel:
1352         t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1353                           if world_db["ThingActions"][id]["TA_NAME"]
1354                              == "use"][0]
1355         t["T_ARGUMENT"] = sel
1356     elif standing_on_food(t):
1357         if  (len(t["T_CARRIES"]) <  # #
1358              world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]):  # #
1359             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1360                                  if world_db["ThingActions"][id]["TA_NAME"]
1361                                  == "pick_up"][0]
1362     else:
1363         going_to_known_food_spot = get_dir_to_target(t, "c")
1364         if not going_to_known_food_spot:
1365             aiming_for_walking_food = get_dir_to_target(t, "a")
1366             if not aiming_for_walking_food:
1367                 get_dir_to_target(t, "s")
1368
1369
1370 def turn_over():
1371     """Run game world and its inhabitants until new player input expected."""
1372     # 7DRL: effort of move action is TA_EFFORT / sqrt(TT_LIFEPOINTS)
1373     id = 0
1374     whilebreaker = False
1375     while world_db["Things"][0]["T_LIFEPOINTS"]:
1376         proliferable_map = world_db["MAP"][:]
1377         for id in [id for id in world_db["Things"]
1378                    if not world_db["Things"][id]["carried"]]:
1379             y = world_db["Things"][id]["T_POSY"]
1380             x = world_db["Things"][id]["T_POSX"]
1381             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord("X")
1382         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1383             if not id in world_db["Things"] or \
1384                world_db["Things"][id]["carried"]:   # May have been consumed or
1385                 continue                            # picked up during turn …
1386             Thing = world_db["Things"][id]
1387             if Thing["T_LIFEPOINTS"]:
1388                 if not Thing["T_COMMAND"]:
1389                     update_map_memory(Thing)
1390                     if 0 == id:
1391                         whilebreaker = True
1392                         break
1393                     ai(Thing)
1394                 try_healing(Thing)
1395                 hunger(Thing)
1396                 if Thing["T_LIFEPOINTS"]:
1397                     Thing["T_PROGRESS"] += 1
1398                     taid = [a for a in world_db["ThingActions"]
1399                               if a == Thing["T_COMMAND"]][0]
1400                     ThingAction = world_db["ThingActions"][taid]
1401                     #if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1402                     effort = ThingAction["TA_EFFORT"]  # #
1403                     if ThingAction["TA_NAME"] == "move":  # #
1404                         type = Thing["T_TYPE"]  # #
1405                         max_hp = (world_db["ThingTypes"][type]  # #
1406                                  ["TT_LIFEPOINTS"])  # #
1407                         effort = int(effort / math.sqrt(max_hp))  # #
1408                     if Thing["T_PROGRESS"] == effort:  # #
1409                         eval("actor_" + ThingAction["TA_NAME"])(Thing)
1410                         Thing["T_COMMAND"] = 0
1411                         Thing["T_PROGRESS"] = 0
1412             thingproliferation(Thing, proliferable_map)
1413         if whilebreaker:
1414             break
1415         world_db["TURN"] += 1
1416
1417
1418 def new_Thing(type, pos=(0, 0)):
1419     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1420     thing = {
1421         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1422         "T_ARGUMENT": 0,
1423         "T_PROGRESS": 0,
1424         "T_SATIATION": 0,
1425         "T_COMMAND": 0,
1426         "T_PLAYERDROP": 0,  # #
1427         "T_TYPE": type,
1428         "T_POSY": pos[0],
1429         "T_POSX": pos[1],
1430         "T_CARRIES": [],
1431         "carried": False,
1432         "T_MEMTHING": [],
1433         "T_MEMMAP": False,
1434         "T_MEMDEPTHMAP": False,
1435         "fovmap": False
1436     }
1437     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1438         build_fov_map(thing)
1439     return thing
1440
1441
1442 def id_setter(id, category, id_store=False, start_at_1=False):
1443     """Set ID of object of category to manipulate ID unused? Create new one.
1444
1445     The ID is stored as id_store.id (if id_store is set). If the integer of the
1446     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1447     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1448     always returned when no new object is created, otherwise the new object's
1449     ID.
1450     """
1451     min = 0 if start_at_1 else -1
1452     if str == type(id):
1453         id = integer_test(id, min)
1454     if None != id:
1455         if id in world_db[category]:
1456             if id_store:
1457                 id_store.id = id
1458             return None
1459         else:
1460             if (start_at_1 and 0 == id) \
1461                or ((not start_at_1) and (id < 0)):
1462                 id = 0 if start_at_1 else -1
1463                 while 1:
1464                     id = id + 1
1465                     if id not in world_db[category]:
1466                         break
1467             if id_store:
1468                 id_store.id = id
1469     return id
1470
1471
1472 def command_ping():
1473     """Send PONG line to server output file."""
1474     strong_write(io_db["file_out"], "PONG\n")
1475
1476
1477 def command_quit():
1478     """Abort server process."""
1479     if None == opts.replay:
1480         if world_db["WORLD_ACTIVE"]:
1481             save_world()
1482         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1483     raise SystemExit("received QUIT command")
1484
1485
1486 def command_thingshere(str_y, str_x):
1487     """Write to out file list of Things known to player at coordinate y, x."""
1488     # 7DRL: terrain, too
1489     if world_db["WORLD_ACTIVE"]:
1490         y = integer_test(str_y, 0, 255)
1491         x = integer_test(str_x, 0, 255)
1492         length = world_db["MAP_LENGTH"]
1493         if None != y and None != x and y < length and x < length:
1494             pos = (y * world_db["MAP_LENGTH"]) + x
1495             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1496             pos = y * world_db["MAP_LENGTH"] + x;  # #
1497             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1498                 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1499                               for id in world_db["Things"]
1500                               if not world_db["Things"][id]["carried"]
1501                               if world_db["Things"][id]["T_TYPE"] == tid
1502                               if y == world_db["Things"][id]["T_POSY"]
1503                               if x == world_db["Things"][id]["T_POSX"]]:
1504                     type = world_db["Things"][id]["T_TYPE"]
1505                     name = world_db["ThingTypes"][type]["TT_NAME"]
1506                     strong_write(io_db["file_out"], name + "\n")
1507             else:
1508                 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1509                               for mt in world_db["Things"][0]["T_MEMTHING"]
1510                               if mt[0] == tid if y == mt[1] if x == mt[2]]:
1511                     name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1512                     strong_write(io_db["file_out"], name + "\n")
1513             if world_db["Things"][0]["T_MEMMAP"][pos] == ord("~"):  # #
1514                 name = "(terrain: SEA)"  # #
1515             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("."):  # #
1516                 name = "(terrain: EARTH)"  # #
1517             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord(":"):  # #
1518                 name = "(terrain: SOIL)"  # #
1519             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("X"):  # #
1520                 name = "(terrain: TREE)"  # #
1521             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("|"):  # #
1522                 name = "(terrain: WALL)"  # #
1523             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("_"):  # #
1524                 name = "(terrain: ALTAR)"  # #
1525             else:  # #
1526                 name = "(?)"  # #
1527             strong_write(io_db["file_out"], name + "\n")  # #
1528             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1529         else:
1530             print("Ignoring: Invalid map coordinates.")
1531     else:
1532         print("Ignoring: Command only works on existing worlds.")
1533
1534
1535 def play_commander(action, args=False):
1536     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1537
1538     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1539     """
1540
1541     def set_command():
1542
1543         if action == "drop":
1544             t = world_db["Things"][0]
1545             if 0 == len(t["T_CARRIES"]):
1546                 log("You have nothing to drop in your inventory.")
1547                 return
1548
1549         elif action == "pick_up":
1550             t = world_db["Things"][0]
1551             ids = [id for id in world_db["Things"] if id
1552                    if not world_db["Things"][id]["carried"]
1553                    if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1554                    if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
1555             if not len(ids):
1556                 log("No object to pick up.")
1557                 return
1558             used_slots = len(t["T_CARRIES"]) # #
1559             if used_slots >= world_db["ThingTypes"][t["T_TYPE"]] \
1560                 ["TT_STORAGE"]: # #
1561                 log("Can't pick up: No storage room to carry anything more.")
1562                 return
1563
1564         elif action == "move":
1565             t = world_db["Things"][0]
1566             passable = False
1567             move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
1568                                              t["T_POSY"], t["T_POSX"])
1569             if 1 == move_result[0]:
1570                 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
1571                 if (ord("X") == world_db["MAP"][pos]  # #
1572                     or ord("|") == world_db["MAP"][pos]):  # #
1573                     carries_axe = False  # #
1574                     for id in t["T_CARRIES"]:  # #
1575                         type = world_db["Things"][id]["T_TYPE"]  # #
1576                         if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":  # #
1577                             carries_axe = True  # #
1578                             break  # #
1579                     if not carries_axe:  # #
1580                         log("You can't move there.")
1581                         return
1582             else:
1583                 log("You can't move there.")
1584                 return
1585
1586         elif action == "use":
1587             t = world_db["Things"][0]
1588             if len(t["T_CARRIES"]):
1589                 id = t["T_CARRIES"][t["T_ARGUMENT"]]
1590                 type = world_db["Things"][id]["T_TYPE"]
1591                 if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":
1592                     log("To use this item for chopping, move towards a tree "
1593                          + "while carrying it in your inventory.")
1594                     return
1595                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "carpentry":
1596                     pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1597                     if (world_db["MAP"][pos] == ord("X")  # #
1598                         or world_db["MAP"][pos] == ord("|")):  # #
1599                         log("Can't build when standing on barrier.")  # #
1600                         return
1601                     for id in [id for id in world_db["Things"]
1602                                if not world_db["Things"][id] == t
1603                                if not world_db["Things"][id]["carried"]
1604                                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1605                                if world_db["Things"][id]["T_POSX"] ==t["T_POSX"]]:
1606                          log("Can't build when standing objects.")  # #
1607                          return
1608                     for id in t["T_CARRIES"]:  # #
1609                         type_tool = world_db["Things"][id]["T_TYPE"]  # #
1610                         if (world_db["ThingTypes"][type_tool]["TT_TOOL"]  # #
1611                             == "carpentry"):  # #
1612                             break  # #
1613                     wood_id = None  # #
1614                     for id in t["T_CARRIES"]:  # #
1615                         type_material = world_db["Things"][id]["T_TYPE"]  # #
1616                         if (world_db["ThingTypes"][type_material]["TT_TOOL"]  # #
1617                             == "wood"):  # #
1618                             wood_id = id  # #
1619                             break  # #
1620                     if wood_id == None:  # #
1621                         log("You can't use a "  # #
1622                             + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1623                             + " without some wood in your inventory.")  # #
1624                         return
1625                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "fertilizer":  # #
1626                     pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1627                     if not world_db["MAP"][pos] == ord("."):
1628                         log("Can only make soil out of non-soil earth.")
1629                         return
1630                 elif world_db["ThingTypes"][type]["TT_TOOL"] == "wood":  # #
1631                         log("To use wood, you need a carpentry tool.")  # #
1632                         return
1633             else:
1634                 log("You have nothing to use in your inventory.")
1635                 return
1636
1637
1638         id = [x for x in world_db["ThingActions"]
1639                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1640         world_db["Things"][0]["T_COMMAND"] = id
1641         turn_over()
1642
1643     def set_command_and_argument_int(str_arg):
1644         val = integer_test(str_arg, 0, 255)
1645         if None != val:
1646             world_db["Things"][0]["T_ARGUMENT"] = val
1647             set_command()
1648
1649     def set_command_and_argument_movestring(str_arg):
1650         if str_arg in directions_db:
1651             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1652             set_command()
1653         else:
1654             print("Ignoring: Argument must be valid direction string.")
1655
1656     if action == "move":
1657         return set_command_and_argument_movestring
1658     elif args:
1659         return set_command_and_argument_int
1660     else:
1661         return set_command
1662
1663
1664 def command_seedrandomness(seed_string):
1665     """Set rand seed to int(seed_string)."""
1666     val = integer_test(seed_string, 0, 4294967295)
1667     if None != val:
1668         rand.seed = val
1669
1670
1671 def command_makeworld(seed_string):
1672     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1673
1674     Seed rand with seed. Do more only with a "wait" ThingAction and
1675     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1676     world_db["Things"] emptied, call make_map() and set
1677     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1678     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1679     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1680     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1681     Call log_help().
1682     """
1683
1684     # def free_pos(plant=False):
1685     def free_pos(plant=False):  # #
1686         i = 0
1687         while 1:
1688             err = "Space to put thing on too hard to find. Map too small?"
1689             while 1:
1690                 y = rand.next() % world_db["MAP_LENGTH"]
1691                 x = rand.next() % world_db["MAP_LENGTH"]
1692                 pos = y * world_db["MAP_LENGTH"] + x;
1693                 if (not plant  # #
1694                     and "." == chr(world_db["MAP"][pos])) \
1695                    or ":" == chr(world_db["MAP"][pos]):  # #
1696                     break
1697                 i += 1
1698                 if i == 65535:
1699                     raise SystemExit(err)
1700             # Replica of C code, wrongly ignores animatedness of new Thing.
1701             pos_clear = (0 == len([id for id in world_db["Things"]
1702                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1703                                    if world_db["Things"][id]["T_POSY"] == y
1704                                    if world_db["Things"][id]["T_POSX"] == x]))
1705             if pos_clear:
1706                 break
1707         return (y, x)
1708
1709     val = integer_test(seed_string, 0, 4294967295)
1710     if None == val:
1711         return
1712     rand.seed = val
1713     player_will_be_generated = False
1714     playertype = world_db["PLAYER_TYPE"]
1715     for ThingType in world_db["ThingTypes"]:
1716         if playertype == ThingType:
1717             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1718                 player_will_be_generated = True
1719             break
1720     if not player_will_be_generated:
1721         print("Ignoring: No player type with start number >0 defined.")
1722         return
1723     wait_action = False
1724     for ThingAction in world_db["ThingActions"]:
1725         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1726             wait_action = True
1727     if not wait_action:
1728         print("Ignoring: No thing action with name 'wait' defined.")
1729         return
1730     for name in specials:  # #
1731         if world_db[name] not in world_db["ThingTypes"]:  # #
1732             print("Ignoring: No valid " + name + " set.")  # #
1733             return  # #
1734     world_db["Things"] = {}
1735     make_map()
1736     world_db["WORLD_ACTIVE"] = 1
1737     world_db["TURN"] = 1
1738     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1739         id = id_setter(-1, "Things")
1740         world_db["Things"][id] = new_Thing(playertype, free_pos())
1741     if not world_db["Things"][0]["fovmap"]:
1742         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1743         world_db["Things"][0]["fovmap"] = empty_fovmap
1744     update_map_memory(world_db["Things"][0])
1745     for type in world_db["ThingTypes"]:
1746         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1747             if type != playertype:
1748                 id = id_setter(-1, "Things")
1749                 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"]  # #
1750                 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1751     strong_write(io_db["file_out"], "NEW_WORLD\n")
1752     log_help()
1753
1754
1755 def command_maplength(maplength_string):
1756     """Redefine map length. Invalidate map, therefore lose all things on it."""
1757     val = integer_test(maplength_string, 1, 256)
1758     if None != val:
1759         world_db["MAP_LENGTH"] = val
1760         world_db["MAP"] = False
1761         set_world_inactive()
1762         world_db["Things"] = {}
1763         libpr.set_maplength(val)
1764
1765
1766 def command_worldactive(worldactive_string):
1767     """Toggle world_db["WORLD_ACTIVE"] if possible.
1768
1769     An active world can always be set inactive. An inactive world can only be
1770     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1771     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1772     Also call log_help().
1773     """
1774     # 7DRL: altar must be on map, and specials must be set for active world.
1775     val = integer_test(worldactive_string, 0, 1)
1776     if None != val:
1777         if 0 != world_db["WORLD_ACTIVE"]:
1778             if 0 == val:
1779                 set_world_inactive()
1780             else:
1781                 print("World already active.")
1782         elif 0 == world_db["WORLD_ACTIVE"]:
1783             wait_exists = False
1784             for ThingAction in world_db["ThingActions"]:
1785                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1786                     wait_exists = True
1787                     break
1788             player_exists = False
1789             for Thing in world_db["Things"]:
1790                 if 0 == Thing:
1791                     player_exists = True
1792                     break
1793             specials_set = True  # #
1794             for name in specials:  # #
1795                 if world_db[name] not in world_db["ThingTypes"]:  # #
1796                     specials_set = False  # #
1797             altar_found = False  # #
1798             if world_db["MAP"]:  # #
1799                 pos = world_db["MAP"].find(b'_')  # #
1800                 if pos > 0:  # #
1801                     y = int(pos / world_db["MAP_LENGTH"])  # #
1802                     x = pos % world_db["MAP_LENGTH"]  # #
1803                     world_db["altar"] = (y, x)  # #
1804                     altar_found = True  # #
1805             if wait_exists and player_exists and world_db["MAP"] \
1806                and specials_set:  # #
1807                 for id in world_db["Things"]:
1808                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1809                         build_fov_map(world_db["Things"][id])
1810                         if 0 == id:
1811                             update_map_memory(world_db["Things"][id], False)
1812                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1813                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1814                     world_db["Things"][0]["fovmap"] = empty_fovmap
1815                 world_db["WORLD_ACTIVE"] = 1
1816                 log_help()
1817             else:
1818                 print("Ignoring: Not all conditions for world activation met.")
1819
1820
1821 def specialtypesetter(name):  # #
1822     """Setter world_db[name], deactivating world if set int no ThingType."""
1823     def helper(str_int):
1824         val = integer_test(str_int, 0)
1825         if None != val:
1826             world_db[name] = val
1827             if world_db["WORLD_ACTIVE"] \
1828                and world_db[name] not in world_db["ThingTypes"]:
1829                 world_db["WORLD_ACTIVE"] = 0
1830                 print(name + " fits no known ThingType, deactivating world.")
1831     return helper
1832
1833
1834 def test_for_id_maker(object, category):
1835     """Return decorator testing for object having "id" attribute."""
1836     def decorator(f):
1837         def helper(*args):
1838             if hasattr(object, "id"):
1839                 f(*args)
1840             else:
1841                 print("Ignoring: No " + category +
1842                       " defined to manipulate yet.")
1843         return helper
1844     return decorator
1845
1846
1847 def command_tid(id_string):
1848     """Set ID of Thing to manipulate. ID unused? Create new one.
1849
1850     Default new Thing's type to the first available ThingType, others: zero.
1851     """
1852     id = id_setter(id_string, "Things", command_tid)
1853     if None != id:
1854         if world_db["ThingTypes"] == {}:
1855             print("Ignoring: No ThingType to settle new Thing in.")
1856             return
1857         type = list(world_db["ThingTypes"].keys())[0]
1858         world_db["Things"][id] = new_Thing(type)
1859
1860
1861 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1862
1863
1864 @test_Thing_id
1865 def command_tcommand(str_int):
1866     """Set T_COMMAND of selected Thing."""
1867     val = integer_test(str_int, 0)
1868     if None != val:
1869         if 0 == val or val in world_db["ThingActions"]:
1870             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1871         else:
1872             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1873
1874
1875 @test_Thing_id
1876 def command_ttype(str_int):
1877     """Set T_TYPE of selected Thing."""
1878     val = integer_test(str_int, 0)
1879     if None != val:
1880         if val in world_db["ThingTypes"]:
1881             world_db["Things"][command_tid.id]["T_TYPE"] = val
1882         else:
1883             print("Ignoring: ThingType ID belongs to no known ThingType.")
1884
1885
1886 @test_Thing_id
1887 def command_tcarries(str_int):
1888     """Append int(str_int) to T_CARRIES of selected Thing.
1889
1890     The ID int(str_int) must not be of the selected Thing, and must belong to a
1891     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1892     """
1893     val = integer_test(str_int, 0)
1894     if None != val:
1895         if val == command_tid.id:
1896             print("Ignoring: Thing cannot carry itself.")
1897         elif val in world_db["Things"] \
1898              and not world_db["Things"][val]["carried"]:
1899             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1900             world_db["Things"][val]["carried"] = True
1901         else:
1902             print("Ignoring: Thing not available for carrying.")
1903     # Note that the whole carrying structure is different from the C version:
1904     # Carried-ness is marked by a "carried" flag, not by Things containing
1905     # Things internally.
1906
1907
1908 @test_Thing_id
1909 def command_tmemthing(str_t, str_y, str_x):
1910     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1911
1912     The type must fit to an existing ThingType, and the position into the map.
1913     """
1914     type = integer_test(str_t, 0)
1915     posy = integer_test(str_y, 0, 255)
1916     posx = integer_test(str_x, 0, 255)
1917     if None != type and None != posy and None != posx:
1918         if type not in world_db["ThingTypes"] \
1919            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1920             print("Ignoring: Illegal value for thing type or position.")
1921         else:
1922             memthing = (type, posy, posx)
1923             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1924
1925
1926 def setter_map(maptype):
1927     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1928
1929     If no map of maptype exists yet, initialize it with ' ' bytes first.
1930     """
1931
1932     def valid_map_line(str_int, mapline):
1933         val = integer_test(str_int, 0, 255)
1934         if None != val:
1935             if val >= world_db["MAP_LENGTH"]:
1936                 print("Illegal value for map line number.")
1937             elif len(mapline) != world_db["MAP_LENGTH"]:
1938                 print("Map line length is unequal map width.")
1939             else:
1940                 return val
1941         return None
1942
1943     def nonThingMap_helper(str_int, mapline):
1944         val = valid_map_line(str_int, mapline)
1945         if None != val:
1946             length = world_db["MAP_LENGTH"]
1947             if not world_db["MAP"]:
1948                 map = bytearray(b' ' * (length ** 2))
1949             else:
1950                 map = world_db["MAP"]
1951             map[val * length:(val * length) + length] = mapline.encode()
1952             if not world_db["MAP"]:
1953                 world_db["MAP"] = map
1954
1955     @test_Thing_id
1956     def ThingMap_helper(str_int, mapline):
1957         val = valid_map_line(str_int, mapline)
1958         if None != val:
1959             length = world_db["MAP_LENGTH"]
1960             if not world_db["Things"][command_tid.id][maptype]:
1961                 map = bytearray(b' ' * (length ** 2))
1962             else:
1963                 map = world_db["Things"][command_tid.id][maptype]
1964             map[val * length:(val * length) + length] = mapline.encode()
1965             if not world_db["Things"][command_tid.id][maptype]:
1966                 world_db["Things"][command_tid.id][maptype] = map
1967
1968     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1969
1970
1971 def setter_tpos(axis):
1972     """Generate setter for T_POSX or  T_POSY of selected Thing.
1973
1974     If world is active, rebuilds animate things' fovmap, player's memory map.
1975     """
1976     @test_Thing_id
1977     def helper(str_int):
1978         val = integer_test(str_int, 0, 255)
1979         if None != val:
1980             if val < world_db["MAP_LENGTH"]:
1981                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1982                 if world_db["WORLD_ACTIVE"] \
1983                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1984                     build_fov_map(world_db["Things"][command_tid.id])
1985                     if 0 == command_tid.id:
1986                         update_map_memory(world_db["Things"][command_tid.id])
1987             else:
1988                 print("Ignoring: Position is outside of map.")
1989     return helper
1990
1991
1992 def command_ttid(id_string):
1993     """Set ID of ThingType to manipulate. ID unused? Create new one.
1994
1995     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
1996     "", others: 0.
1997     """
1998     id = id_setter(id_string, "ThingTypes", command_ttid)
1999     if None != id:
2000         world_db["ThingTypes"][id] = {
2001             "TT_NAME": "(none)",
2002             "TT_TOOLPOWER": 0,
2003             "TT_LIFEPOINTS": 0,
2004             "TT_PROLIFERATE": 0,
2005             "TT_START_NUMBER": 0,
2006             "TT_STORAGE": 0, # #
2007             "TT_SYMBOL": "?",
2008             "TT_CORPSE_ID": id,
2009             "TT_TOOL": ""
2010         }
2011
2012
2013 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
2014
2015
2016 @test_ThingType_id
2017 def command_ttname(name):
2018     """Set TT_NAME of selected ThingType."""
2019     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
2020
2021
2022 @test_ThingType_id
2023 def command_tttool(name):
2024     """Set TT_TOOL of selected ThingType."""
2025     world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
2026
2027
2028 @test_ThingType_id
2029 def command_ttsymbol(char):
2030     """Set TT_SYMBOL of selected ThingType. """
2031     if 1 == len(char):
2032         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
2033     else:
2034         print("Ignoring: Argument must be single character.")
2035
2036
2037 @test_ThingType_id
2038 def command_ttcorpseid(str_int):
2039     """Set TT_CORPSE_ID of selected ThingType."""
2040     val = integer_test(str_int, 0)
2041     if None != val:
2042         if val in world_db["ThingTypes"]:
2043             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
2044         else:
2045             print("Ignoring: Corpse ID belongs to no known ThignType.")
2046
2047
2048 def command_taid(id_string):
2049     """Set ID of ThingAction to manipulate. ID unused? Create new one.
2050
2051     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
2052     """
2053     id = id_setter(id_string, "ThingActions", command_taid, True)
2054     if None != id:
2055         world_db["ThingActions"][id] = {
2056             "TA_EFFORT": 1,
2057             "TA_NAME": "wait"
2058         }
2059
2060
2061 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
2062
2063
2064 @test_ThingAction_id
2065 def command_taname(name):
2066     """Set TA_NAME of selected ThingAction.
2067
2068     The name must match a valid thing action function. If after the name
2069     setting no ThingAction with name "wait" remains, call set_world_inactive().
2070     """
2071     if name == "wait" or name == "move" or name == "use" or name == "drop" \
2072        or name == "pick_up":
2073         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
2074         if 1 == world_db["WORLD_ACTIVE"]:
2075             wait_defined = False
2076             for id in world_db["ThingActions"]:
2077                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
2078                     wait_defined = True
2079                     break
2080             if not wait_defined:
2081                 set_world_inactive()
2082     else:
2083         print("Ignoring: Invalid action name.")
2084     # In contrast to the original,naming won't map a function to a ThingAction.
2085
2086
2087 def command_ai():
2088     """Call ai() on player Thing, then turn_over()."""
2089     ai(world_db["Things"][0])
2090     turn_over()
2091
2092
2093 """Commands database.
2094
2095 Map command start tokens to ([0]) number of expected command arguments, ([1])
2096 the command's meta-ness (i.e. is it to be written to the record file, is it to
2097 be ignored in replay mode if read from server input file), and ([2]) a function
2098 to be called on it.
2099 """
2100 commands_db = {
2101     "QUIT": (0, True, command_quit),
2102     "PING": (0, True, command_ping),
2103     "THINGS_HERE": (2, True, command_thingshere),
2104     "MAKE_WORLD": (1, False, command_makeworld),
2105     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
2106     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
2107     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
2108     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
2109     "MAP_LENGTH": (1, False, command_maplength),
2110     "WORLD_ACTIVE": (1, False, command_worldactive),
2111     "MAP": (2, False, setter_map("MAP")),
2112     "FAVOR_STAGE": (1, False, setter(None, "FAVOR_STAGE", 0, 65535)),  # #
2113     "SLIPPERS": (1, False, specialtypesetter("SLIPPERS")),  # #
2114     "PLANT_0": (1, False, specialtypesetter("PLANT_0")),  # #
2115     "PLANT_1": (1, False, specialtypesetter("PLANT_1")),  # #
2116     "LUMBER": (1, False, specialtypesetter("LUMBER")),  # #
2117     "TOOL_0": (1, False, specialtypesetter("TOOL_0")),  # #
2118     "TOOL_1": (1, False, specialtypesetter("TOOL_1")),  # #
2119     "EMPATHY": (1, False, setter(None, "EMPATHY", 0, 1)),  # #
2120     "TA_ID": (1, False, command_taid),
2121     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
2122     "TA_NAME": (1, False, command_taname),
2123     "TT_ID": (1, False, command_ttid),
2124     "TT_NAME": (1, False, command_ttname),
2125     "TT_TOOL": (1, False, command_tttool),
2126     "TT_SYMBOL": (1, False, command_ttsymbol),
2127     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
2128     "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
2129     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
2130                                          0, 255)),
2131     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
2132                                         0, 65535)),
2133     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
2134     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)),  # #
2135     "T_ID": (1, False, command_tid),
2136     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
2137     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
2138     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
2139     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
2140     "T_COMMAND": (1, False, command_tcommand),
2141     "T_TYPE": (1, False, command_ttype),
2142     "T_CARRIES": (1, False, command_tcarries),
2143     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
2144     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
2145     "T_MEMTHING": (3, False, command_tmemthing),
2146     "T_POSY": (1, False, setter_tpos("Y")),
2147     "T_POSX": (1, False, setter_tpos("X")),
2148     "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)),  # #
2149     "wait": (0, False, play_commander("wait")),
2150     "move": (1, False, play_commander("move")),
2151     "pick_up": (0, False, play_commander("pick_up")),
2152     "drop": (1, False, play_commander("drop", True)),
2153     "use": (1, False, play_commander("use", True)),
2154     "ai": (0, False, command_ai)
2155 }
2156 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
2157
2158
2159 """World state database. With sane default values. (Randomness is in rand.)"""
2160 world_db = {
2161     "TURN": 0,
2162     "MAP_LENGTH": 64,
2163     "PLAYER_TYPE": 0,
2164     "WORLD_ACTIVE": 0,
2165     "GOD_FAVOR": 0,  # #
2166     "MAP": False,
2167     "FAVOR_STAGE": 0,  # #
2168     "SLIPPERS": 0,  # #
2169     "PLANT_0": 0,  # #
2170     "PLANT_1": 0,  # #
2171     "LUMBER": 0,  # #
2172     "TOOL_0": 0,  # #
2173     "TOOL_1": 0,  # #
2174     "EMPATHY": 1,  # #
2175     "ThingActions": {},
2176     "ThingTypes": {},
2177     "Things": {}
2178 }
2179
2180 # 7DRL-specific!
2181 """Special type settings."""
2182 specials = ["SLIPPERS", "PLANT_0", "PLANT_1", "LUMBER", "TOOL_0", "TOOL_1"]  # #
2183
2184 """Mapping of direction names to internal direction chars."""
2185 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
2186                  "west": "s", "north-west": "w", "north-east": "e"}
2187
2188 """File IO database."""
2189 io_db = {
2190     "path_save": "save",
2191     "path_record": "record_save",
2192     "path_worldconf": "confserver/world",
2193     "path_server": "server/",
2194     "path_in": "server/in",
2195     "path_out": "server/out",
2196     "path_worldstate": "server/worldstate",
2197     "tmp_suffix": "_tmp",
2198     "kicked_by_rival": False,
2199     "worldstate_updateable": False
2200 }
2201
2202
2203 try:
2204     libpr = prep_library()
2205     rand = RandomnessIO()
2206     opts = parse_command_line_arguments()
2207     if opts.savefile:
2208         io_db["path_save"] = opts.savefile
2209         io_db["path_record"] = "record_" + opts.savefile
2210     setup_server_io()
2211     if opts.verbose:
2212         io_db["verbose"] = True
2213     if None != opts.replay:
2214         replay_game()
2215     else:
2216         play_game()
2217 except SystemExit as exit:
2218     print("ABORTING: " + exit.args[0])
2219 except:
2220     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
2221     raise
2222 finally:
2223     cleanup_server_io()