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