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