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