home · contact · privacy
Fix bug in thingproliferation() (using "." instead of ":" cell targets).
[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 obey(command, prefix, replay=False, do_record=False):
103     """Call function from commands_db mapped to command's first token.
104
105     Tokenize command string with shlex.split(comments=True). If replay is set,
106     a non-meta command from the commands_db merely triggers obey() on the next
107     command from the records file. If not, non-meta commands set
108     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
109     do_record is set, are recorded to io_db["record_chunk"], and save_world()
110     is called (and io_db["record_chunk"] written) if 15 seconds have passed
111     since the last time it was called. The prefix string is inserted into the
112     server's input message between its beginning 'input ' and ':'. All activity
113     is preceded by a server_test() call. Commands that start with a lowercase
114     letter are ignored when world_db["WORLD_ACTIVE"] is False/0.
115     """
116     server_test()
117     if io_db["verbose"]:
118         print("input " + prefix + ": " + command)
119     try:
120         tokens = shlex.split(command, comments=True)
121     except ValueError as err:
122         print("Can't tokenize command string: " + str(err) + ".")
123         return
124     if len(tokens) > 0 and tokens[0] in commands_db \
125        and len(tokens) == commands_db[tokens[0]][0] + 1:
126         if commands_db[tokens[0]][1]:
127             commands_db[tokens[0]][2](*tokens[1:])
128         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
129             print("Ignoring lowercase-starting commands when world inactive.")
130         elif replay:
131             print("Due to replay mode, reading command as 'go on in record'.")
132             line = io_db["file_record"].readline()
133             if len(line) > 0:
134                 obey(line.rstrip(), io_db["file_record"].prefix
135                      + str(io_db["file_record"].line_n))
136                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
137             else:
138                 print("Reached end of record file.")
139         else:
140             commands_db[tokens[0]][2](*tokens[1:])
141             if do_record:
142                 io_db["record_chunk"] += command + "\n"
143                 if time.time() > io_db["save_wait"] + 15:
144                     atomic_write(io_db["path_record"], io_db["record_chunk"],
145                                  do_append=True)
146                     if world_db["WORLD_ACTIVE"]:
147                         save_world()
148                     io_db["record_chunk"] = ""
149                     io_db["save_wait"] = time.time()
150             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
151     elif 0 != len(tokens):
152         print("Invalid command/argument, or bad number of tokens.")
153
154
155 def atomic_write(path, text, do_append=False, delete=True):
156     """Atomic write of text to file at path, appended if do_append is set."""
157     path_tmp = path + io_db["tmp_suffix"]
158     mode = "w"
159     if do_append:
160         mode = "a"
161         if os.access(path, os.F_OK):
162             shutil.copyfile(path, path_tmp)
163     file = open(path_tmp, mode)
164     strong_write(file, text)
165     file.close()
166     if delete and os.access(path, os.F_OK):
167         os.remove(path)
168     os.rename(path_tmp, path)
169
170
171 def save_world():
172     """Save all commands needed to reconstruct current world state."""
173
174     def quote(string):
175         string = string.replace("\u005C", '\u005C\u005C')
176         return '"' + string.replace('"', '\u005C"') + '"'
177
178     def mapsetter(key):
179         def helper(id=None):
180             string = ""
181             if key == "MAP" or world_db["Things"][id][key]:
182                 map = world_db["MAP"] if key == "MAP" \
183                                       else world_db["Things"][id][key]
184                 length = world_db["MAP_LENGTH"]
185                 for i in range(length):
186                     line = map[i * length:(i * length) + length].decode()
187                     string = string + key + " " + str(i) + " " + quote(line) \
188                              + "\n"
189             return string
190         return helper
191
192     def memthing(id):
193         string = ""
194         for memthing in world_db["Things"][id]["T_MEMTHING"]:
195             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
196                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
197         return string
198
199     def helper(category, id_string, special_keys={}):
200         string = ""
201         for id in world_db[category]:
202             string = string + id_string + " " + str(id) + "\n"
203             for key in world_db[category][id]:
204                 if not key in special_keys:
205                     x = world_db[category][id][key]
206                     argument = quote(x) if str == type(x) else str(x)
207                     string = string + key + " " + argument + "\n"
208                 elif special_keys[key]:
209                     string = string + special_keys[key](id)
210         return string
211
212     string = ""
213     for key in world_db:
214         if dict != type(world_db[key]) \
215            and key != "MAP" and key != "WORLD_ACTIVE":
216             string = string + key + " " + str(world_db[key]) + "\n"
217     string = string + mapsetter("MAP")()
218     string = string + helper("ThingActions", "TA_ID")
219     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
220     for id in world_db["ThingTypes"]:
221         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
222                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
223     string = string + helper("Things", "T_ID",
224                              {"T_CARRIES": False, "carried": False,
225                               "T_MEMMAP": mapsetter("T_MEMMAP"),
226                               "T_MEMTHING": memthing, "fovmap": False,
227                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
228     for id in world_db["Things"]:
229         if [] != world_db["Things"][id]["T_CARRIES"]:
230             string = string + "T_ID " + str(id) + "\n"
231             for carried_id in world_db["Things"][id]["T_CARRIES"]:
232                 string = string + "T_CARRIES " + str(carried_id) + "\n"
233     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
234              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
235     atomic_write(io_db["path_save"], string)
236
237
238 def obey_lines_in_file(path, name, do_record=False):
239     """Call obey() on each line of path's file, use name in input prefix."""
240     file = open(path, "r")
241     line_n = 1
242     for line in file.readlines():
243         obey(line.rstrip(), name + "file line " + str(line_n),
244              do_record=do_record)
245         line_n = line_n + 1
246     file.close()
247
248
249 def parse_command_line_arguments():
250     """Return settings values read from command line arguments."""
251     parser = argparse.ArgumentParser()
252     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
253                         action='store')
254     parser.add_argument('-l', nargs="?", const="save", dest='savefile',
255                         action="store")
256     parser.add_argument('-v', dest='verbose', action='store_true')
257     opts, unknown = parser.parse_known_args()
258     return opts
259
260
261 def server_test():
262     """Ensure valid server out file belonging to current process.
263
264     This is done by comparing io_db["teststring"] to what's found at the start
265     of the current file at io_db["path_out"]. On failure, set
266     io_db["kicked_by_rival"] and raise SystemExit.
267     """
268     if not os.access(io_db["path_out"], os.F_OK):
269         raise SystemExit("Server output file has disappeared.")
270     file = open(io_db["path_out"], "r")
271     test = file.readline().rstrip("\n")
272     file.close()
273     if test != io_db["teststring"]:
274         io_db["kicked_by_rival"] = True
275         msg = "Server test string in server output file does not match. This" \
276               " indicates that the current server process has been " \
277               "superseded by another one."
278         raise SystemExit(msg)
279
280
281 def read_command():
282     """Return next newline-delimited command from server in file.
283
284     Keep building return string until a newline is encountered. Pause between
285     unsuccessful reads, and after too much waiting, run server_test().
286     """
287     wait_on_fail = 0.03333
288     max_wait = 5
289     now = time.time()
290     command = ""
291     while True:
292         add = io_db["file_in"].readline()
293         if len(add) > 0:
294             command = command + add
295             if len(command) > 0 and "\n" == command[-1]:
296                 command = command[:-1]
297                 break
298         else:
299             time.sleep(wait_on_fail)
300             if now + max_wait < time.time():
301                 server_test()
302                 now = time.time()
303     return command
304
305
306 def try_worldstate_update():
307     """Write worldstate file if io_db["worldstate_updateable"] is set."""
308     if io_db["worldstate_updateable"]:
309
310         def draw_visible_Things(map, run):
311             for id in world_db["Things"]:
312                 type = world_db["Things"][id]["T_TYPE"]
313                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
314                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
315                 if (0 == run and not consumable and not alive) \
316                    or (1 == run and consumable and not alive) \
317                    or (2 == run and alive):
318                     y = world_db["Things"][id]["T_POSY"]
319                     x = world_db["Things"][id]["T_POSX"]
320                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
321                     if 'v' == chr(fovflag):
322                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
323                         map[(y * length) + x] = ord(c)
324
325         def write_map(string, map):
326             for i in range(length):
327                 line = map[i * length:(i * length) + length].decode()
328                 string = string + line + "\n"
329             return string
330
331         inventory = ""
332         if [] == world_db["Things"][0]["T_CARRIES"]:
333             inventory = "(none)\n"
334         else:
335             for id in world_db["Things"][0]["T_CARRIES"]:
336                 type_id = world_db["Things"][id]["T_TYPE"]
337                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
338                 inventory = inventory + name + "\n"
339         # # 7DRL additions:  GOD_MOOD, GOD_FAVOR
340         string = str(world_db["TURN"]) + "\n" + \
341                  str(world_db["GOD_MOOD"]) + "\n" + \
342                  str(world_db["GOD_FAVOR"]) + "\n" + \
343                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
344                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
345                  inventory + "%\n" + \
346                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
347                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
348                  str(world_db["MAP_LENGTH"]) + "\n"
349         length = world_db["MAP_LENGTH"]
350         fov = bytearray(b' ' * (length ** 2))
351         for pos in range(length ** 2):
352             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
353                 fov[pos] = world_db["MAP"][pos]
354         for i in range(3):
355             draw_visible_Things(fov, i)
356         string = write_map(string, fov)
357         mem = world_db["Things"][0]["T_MEMMAP"][:]
358         for i in range(2):
359             for mt in world_db["Things"][0]["T_MEMTHING"]:
360                 consumable = world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]
361                 if (i == 0 and not consumable) or (i == 1 and consumable):
362                     c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
363                     mem[(mt[1] * length) + mt[2]] = ord(c)
364         string = write_map(string, mem)
365         atomic_write(io_db["path_worldstate"], string, delete=False)
366         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
367         io_db["worldstate_updateable"] = False
368
369
370 def replay_game():
371     """Replay game from record file.
372
373     Use opts.replay as breakpoint turn to which to replay automatically before
374     switching to manual input by non-meta commands in server input file
375     triggering further reads of record file. Ensure opts.replay is at least 1.
376     Run try_worldstate_update() before each interactive obey()/read_command().
377     """
378     if opts.replay < 1:
379         opts.replay = 1
380     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
381           " (if so late a turn is to be found).")
382     if not os.access(io_db["path_record"], os.F_OK):
383         raise SystemExit("No record file found to replay.")
384     io_db["file_record"] = open(io_db["path_record"], "r")
385     io_db["file_record"].prefix = "record file line "
386     io_db["file_record"].line_n = 1
387     while world_db["TURN"] < opts.replay:
388         line = io_db["file_record"].readline()
389         if "" == line:
390             break
391         obey(line.rstrip(), io_db["file_record"].prefix
392              + str(io_db["file_record"].line_n))
393         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
394     while True:
395         try_worldstate_update()
396         obey(read_command(), "in file", replay=True)
397
398
399 def play_game():
400     """Play game by server input file commands. Before, load save file found.
401
402     If no save file is found, a new world is generated from the commands in the
403     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
404     command and all that follow via the server input file. Run
405     try_worldstate_update() before each interactive obey()/read_command().
406     """
407     if os.access(io_db["path_save"], os.F_OK):
408         obey_lines_in_file(io_db["path_save"], "save")
409     else:
410         if not os.access(io_db["path_worldconf"], os.F_OK):
411             msg = "No world config file from which to start a new world."
412             raise SystemExit(msg)
413         obey_lines_in_file(io_db["path_worldconf"], "world config ",
414                            do_record=True)
415         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
416     while True:
417         try_worldstate_update()
418         obey(read_command(), "in file", do_record=True)
419
420
421 def make_map():
422     """(Re-)make island map.
423
424     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
425     start with one land cell in the middle, then go into cycle of repeatedly
426     selecting a random sea cell and transforming it into land if it is neighbor
427     to land. The cycle ends when a land cell is due to be created at the map's
428     border. Then put some trees on the map (TODO: more precise algorithm desc).
429     """
430     # 7DRL: Also add some ":" cells as land to which plants may proliferate.
431
432     def is_neighbor(coordinates, type):
433         y = coordinates[0]
434         x = coordinates[1]
435         length = world_db["MAP_LENGTH"]
436         ind = y % 2
437         diag_west = x + (ind > 0)
438         diag_east = x + (ind < (length - 1))
439         pos = (y * length) + x
440         if (y > 0 and diag_east
441             and type == chr(world_db["MAP"][pos - length + ind])) \
442            or (x < (length - 1)
443                and type == chr(world_db["MAP"][pos + 1])) \
444            or (y < (length - 1) and diag_east
445                and type == chr(world_db["MAP"][pos + length + ind])) \
446            or (y > 0 and diag_west
447                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
448            or (x > 0
449                and type == chr(world_db["MAP"][pos - 1])) \
450            or (y < (length - 1) and diag_west
451                and type == chr(world_db["MAP"][pos + length - (not ind)])):
452             return True
453         return False
454
455     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
456     length = world_db["MAP_LENGTH"]
457     add_half_width = (not (length % 2)) * int(length / 2)
458     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
459     while (1):
460         y = rand.next() % length
461         x = rand.next() % length
462         pos = (y * length) + x
463         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
464             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
465                 break
466             world_db["MAP"][pos] = ord(".")
467     n_trees = int((length ** 2) / 16)
468     i_trees = 0
469     while (i_trees <= n_trees):
470         single_allowed = rand.next() % 32
471         y = rand.next() % length
472         x = rand.next() % length
473         pos = (y * length) + x
474         if "." == chr(world_db["MAP"][pos]) \
475           and ((not single_allowed) or is_neighbor((y, x), "X")):
476             world_db["MAP"][pos] = ord("X")
477             i_trees += 1
478     # This all-too-precise replica of the original C code misses iter_limit().
479     n_colons = int((length ** 2) / 16)  # #
480     i_colons = 0  # #
481     while (i_colons <= n_colons):  # #
482         single_allowed = rand.next() % 256  # #
483         y = rand.next() % length  # #
484         x = rand.next() % length  # #
485         pos = (y * length) + x  # #
486         if ("." == chr(world_db["MAP"][pos])  # #
487           and ((not single_allowed) or is_neighbor((y, x), ":"))):  # #
488             world_db["MAP"][pos] = ord(":")  # #
489             i_colons += 1  # #
490
491
492 def update_map_memory(t, age_map=True):
493     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
494
495     def age_some_memdepthmap_on_nonfov_cells():
496         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
497         # ord_v = ord("v")
498         # ord_0 = ord("0")
499         # ord_9 = ord("9")
500         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
501         #             if not ord_v == t["fovmap"][pos]
502         #             if ord_0 <= t["T_MEMDEPTHMAP"][pos]
503         #             if ord_9 > t["T_MEMDEPTHMAP"][pos]
504         #             if not rand.next() % (2 **
505         #                                   (t["T_MEMDEPTHMAP"][pos] - 48))]:
506         #     t["T_MEMDEPTHMAP"][pos] += 1
507         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
508         fovmap = c_pointer_to_bytearray(t["fovmap"])
509         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
510
511     if not t["T_MEMMAP"]:
512         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
513     if not t["T_MEMDEPTHMAP"]:
514         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
515     ord_v = ord("v")
516     ord_0 = ord("0")
517     ord_space = ord(" ")
518     for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
519                 if ord_v == t["fovmap"][pos]]:
520         t["T_MEMDEPTHMAP"][pos] = ord_0
521         if ord_space == t["T_MEMMAP"][pos]:
522             t["T_MEMMAP"][pos] = world_db["MAP"][pos]
523     if age_map:
524         age_some_memdepthmap_on_nonfov_cells()
525     t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
526                        if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
527                                                + mt[2]]]
528     for id in [id for id in world_db["Things"]
529                if not world_db["Things"][id]["carried"]]:
530         type = world_db["Things"][id]["T_TYPE"]
531         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
532             y = world_db["Things"][id]["T_POSY"]
533             x = world_db["Things"][id]["T_POSX"]
534             if ord_v == t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]:
535                 t["T_MEMTHING"].append((type, y, x))
536
537
538 def set_world_inactive():
539     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
540     server_test()
541     if os.access(io_db["path_worldstate"], os.F_OK):
542         os.remove(io_db["path_worldstate"])
543     world_db["WORLD_ACTIVE"] = 0
544
545
546 def integer_test(val_string, min, max=None):
547     """Return val_string if possible integer >= min and <= max, else None."""
548     try:
549         val = int(val_string)
550         if val < min or (max is not None and val > max):
551             raise ValueError
552         return val
553     except ValueError:
554         msg = "Ignoring: Please use integer >= " + str(min)
555         if max is not None:
556             msg += " and <= " + str(max)
557         msg += "."
558         print(msg)
559         return None
560
561
562 def setter(category, key, min, max=None):
563     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
564     if category is None:
565         def f(val_string):
566             val = integer_test(val_string, min, max)
567             if None != val:
568                 world_db[key] = val
569     else:
570         if category == "Thing":
571             id_store = command_tid
572             decorator = test_Thing_id
573         elif category == "ThingType":
574             id_store = command_ttid
575             decorator = test_ThingType_id
576         elif category == "ThingAction":
577             id_store = command_taid
578             decorator = test_ThingAction_id
579
580         @decorator
581         def f(val_string):
582             val = integer_test(val_string, min, max)
583             if None != val:
584                 world_db[category + "s"][id_store.id][key] = val
585     return f
586
587
588 def build_fov_map(t):
589     """Build Thing's FOV map."""
590     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
591     fovmap = c_pointer_to_bytearray(t["fovmap"])
592     map = c_pointer_to_bytearray(world_db["MAP"])
593     if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
594         raise RuntimeError("Malloc error in build_fov_Map().")
595
596
597 def decrement_lifepoints(t):
598     """Decrement t's lifepoints by 1, and if to zero, corpse it.
599
600     If t is the player avatar, only blank its fovmap, so that the client may
601     still display memory data. On non-player things, erase fovmap and memory.
602     Dying actors drop all their things.
603     """
604     # # 7DRL: also decrements God's mood; deaths heavily so
605     # # 7DRL: return 1 if death, else 0
606     t["T_LIFEPOINTS"] -= 1
607     world_db["GOD_MOOD"] -= 1  # #
608     if 0 == t["T_LIFEPOINTS"]:
609         sadness = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
610         world_db["GOD_MOOD"] -= sadness  # #        
611         for id in t["T_CARRIES"]:
612             t["T_CARRIES"].remove(id)
613             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
614             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
615             world_db["Things"][id]["carried"] = False
616         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
617         if world_db["Things"][0] == t:
618             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
619             strong_write(io_db["file_out"], "LOG You die.\n")
620         else:
621             t["fovmap"] = False
622             t["T_MEMMAP"] = False
623             t["T_MEMDEPTHMAP"] = False
624             t["T_MEMTHING"] = []
625         return sadness  # #
626     return 0  # #
627
628
629 def add_gods_favor(i): # #
630     """"Add to GOD_FAVOR, multiplied with factor growing log. with GOD_MOOD."""
631     def favor_multiplier(i):
632         x = 100
633         threshold = math.e * x
634         mood = world_db["GOD_MOOD"]
635         if i > 0:
636             if mood > threshold:
637                 i = i * math.log(mood / x)
638             elif -mood > threshold:
639                 i = i / math.log(-mood / x)
640         elif i < 0:
641             if -mood > threshold:
642                 i = i * math.log(-mood / x)
643             if mood > threshold:
644                 i = i / math.log(mood / x)
645         return int(i)
646     world_db["GOD_FAVOR"] += favor_multiplier(i)
647
648
649 def mv_yx_in_dir_legal(dir, y, x):
650     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
651     dir_c = dir.encode("ascii")[0]
652     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
653     if -1 == test:
654         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
655     return (test, libpr.result_y(), libpr.result_x())
656
657
658 def actor_wait(t):
659     """Make t do nothing (but loudly, if player avatar)."""
660     if t == world_db["Things"][0]:
661         strong_write(io_db["file_out"], "LOG You wait.\n")
662
663
664 def actor_move(t):
665     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
666     # # 7DRL: Player wounding (worse: killing) others will lower God's favor.
667     passable = False
668     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
669                                      t["T_POSY"], t["T_POSX"])
670     if 1 == move_result[0]:
671         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
672         passable = "." == chr(world_db["MAP"][pos]) or \
673                    ":" == chr(world_db["MAP"][pos])  # #
674         hitted = [id for id in world_db["Things"]
675                   if world_db["Things"][id] != t
676                   if world_db["Things"][id]["T_LIFEPOINTS"]
677                   if world_db["Things"][id]["T_POSY"] == move_result[1]
678                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
679         if len(hitted):
680             hit_id = hitted[0]
681             if t == world_db["Things"][0]:
682                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
683                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
684                 strong_write(io_db["file_out"], "LOG You wound " + hitted_name
685                                                 + ".\n")
686                 add_gods_favor(-1)  # #
687             elif 0 == hit_id:
688                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
689                 strong_write(io_db["file_out"], "LOG " + hitter_name +
690                                                 " wounds you.\n")
691             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
692             if test and t == world_db["Things"][0]:  # #
693                 add_gods_favor(-test)  # #
694             return
695     dir = [dir for dir in directions_db
696            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
697     if passable:
698         t["T_POSY"] = move_result[1]
699         t["T_POSX"] = move_result[2]
700         for id in t["T_CARRIES"]:
701             world_db["Things"][id]["T_POSY"] = move_result[1]
702             world_db["Things"][id]["T_POSX"] = move_result[2]
703         build_fov_map(t)
704         if t == world_db["Things"][0]:
705             strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
706     elif t == world_db["Things"][0]:
707         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
708
709
710 def actor_pick_up(t):
711     """Make t pick up (topmost?) Thing from ground into inventory."""
712     # Topmostness is actually not defined so far. Picks most nutritious Thing.
713     # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
714     used_slots = len(t["T_CARRIES"]) # #
715     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
716         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
717                if not world_db["Things"][id]["carried"]
718                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
719                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
720         if len(ids):
721             highest_id = ids[0]
722             nutritious = 0
723             for id in ids:
724                 type = world_db["Things"][id]["T_TYPE"]
725                 if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > nutritious:
726                     nutritious = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
727                     highest_id = id
728             world_db["Things"][highest_id]["carried"] = True
729             if (t != world_db["Things"][0] and  # #
730                 world_db["Things"][highest_id]["T_PLAYERDROP"]):  # #
731                 x = world_db["Things"][highest_id]["T_TYPE"]
732                 score = world_db["ThingTypes"][x]["TT_CONSUMABLE"] / 32  # #
733                 add_gods_favor(score)  # #
734                 world_db["Things"][highest_id]["T_PLAYERDROP"] = 0  # #
735             t["T_CARRIES"].append(highest_id)
736             if t == world_db["Things"][0]:
737                 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
738         elif t == world_db["Things"][0]:
739             err = "You try to pick up an object, but there is none."
740             strong_write(io_db["file_out"], "LOG " + err + "\n")
741     elif t == world_db["Things"][0]: # #
742         strong_write(io_db["file_out"], "LOG Can't pick up object: " + # #
743                                         "No storage room to carry more.\n") # #
744
745
746 def actor_drop(t):
747     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
748     # TODO: Handle case where T_ARGUMENT matches nothing.
749     if len(t["T_CARRIES"]):
750         id = t["T_CARRIES"][t["T_ARGUMENT"]]
751         t["T_CARRIES"].remove(id)
752         world_db["Things"][id]["carried"] = False
753         if t == world_db["Things"][0]:
754             strong_write(io_db["file_out"], "LOG You drop an object.\n")
755             world_db["Things"][id]["T_PLAYERDROP"] = 1  # #
756     elif t == world_db["Things"][0]:
757         err = "You try to drop an object, but you own none."
758         strong_write(io_db["file_out"], "LOG " + err + "\n")
759
760
761 def actor_use(t):
762     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
763     # TODO: Handle case where T_ARGUMENT matches nothing.
764     if len(t["T_CARRIES"]):
765         id = t["T_CARRIES"][t["T_ARGUMENT"]]
766         type = world_db["Things"][id]["T_TYPE"]
767         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
768             t["T_CARRIES"].remove(id)
769             del world_db["Things"][id]
770             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
771             if t == world_db["Things"][0]:
772                 strong_write(io_db["file_out"],
773                              "LOG You consume this object.\n")
774         elif t == world_db["Things"][0]:
775             strong_write(io_db["file_out"],
776                          "LOG You try to use this object, but fail.\n")
777     elif t == world_db["Things"][0]:
778         strong_write(io_db["file_out"],
779                      "LOG You try to use an object, but you own none.\n")
780
781
782 def thingproliferation(t, prol_map):
783     """To chance of 1/TT_PROLIFERATE,create  t offspring in open neighbor cell.
784
785     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
786     marked "." in prol_map. If there are several map cell candidates, one is
787     selected randomly.
788     """
789     # # 7DRL: success increments God's mood
790     # # 7DRL: Things proliferate only on ":" ground.
791     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
792     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
793         candidates = []
794         for dir in [directions_db[key] for key in directions_db]:
795             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
796             if mv_result[0] and ord(":") == prol_map[mv_result[1]  # #
797             # if mv_result[0] and ord(".") == prol_map[mv_result[1]
798                                                      * world_db["MAP_LENGTH"]
799                                                      + mv_result[2]]:
800                 candidates.append((mv_result[1], mv_result[2]))
801         if len(candidates):
802             i = rand.next() % len(candidates)
803             id = id_setter(-1, "Things")
804             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
805             world_db["Things"][id] = newT
806             world_db["GOD_MOOD"] += 1  # #
807
808
809 def try_healing(t):
810     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
811
812     On success, decrease satiation score by 32.
813     """
814     # # 7DRL: Successful heals increment God's mood.
815     if t["T_SATIATION"] > 0 \
816        and t["T_LIFEPOINTS"] < \
817            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
818        and 0 == (rand.next() % 31) \
819        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
820                               if world_db["ThingActions"][id]["TA_NAME"] ==
821                                  "wait"][0]:
822         t["T_LIFEPOINTS"] += 1
823         world_db["GOD_MOOD"] += 1  # #
824         t["T_SATIATION"] -= 32
825         if t == world_db["Things"][0]:
826             strong_write(io_db["file_out"], "LOG You heal.\n")
827
828
829 def hunger(t):
830     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
831     if t["T_SATIATION"] > -32768:
832         t["T_SATIATION"] -= 1
833     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
834     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
835         raise RuntimeError("A thing that should not hunger is hungering.")
836     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
837     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
838         if t == world_db["Things"][0]:
839             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
840         decrement_lifepoints(t)
841
842
843 def get_dir_to_target(t, filter):
844     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
845
846     The path-wise nearest target is chosen, via the shortest available path.
847     Target must not be t. On succcess, return positive value, else False.
848     Filters:
849     "a": Thing in FOV is below a certain distance, animate, but of ThingType
850          that is not t's, and starts out weaker than t is; build path as
851          avoiding things of t's ThingType
852     "f": neighbor cell (not inhabited by any animate Thing) further away from
853          animate Thing not further than x steps away and in FOV and of a
854          ThingType that is not t's, and starts out stronger or as strong as t
855          is currently; or (cornered), if no such flight cell, but Thing of
856          above criteria is too near,1 a cell closer to it, or, if less near,
857          just wait
858     "c": Thing in memorized map is consumable
859     "s": memory map cell with greatest-reachable degree of unexploredness
860     """
861
862     def zero_score_map_where_char_on_memdepthmap(c):
863         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
864         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
865         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
866         #     set_map_score(i, 0)
867         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
868         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
869             raise RuntimeError("No score map allocated for "
870                                "zero_score_map_where_char_on_memdepthmap().")
871
872     def set_map_score(pos, score):
873         test = libpr.set_map_score(pos, score)
874         if test:
875             raise RuntimeError("No score map allocated for set_map_score().")
876
877     def get_map_score(pos):
878         result = libpr.get_map_score(pos)
879         if result < 0:
880             raise RuntimeError("No score map allocated for get_map_score().")
881         return result
882
883     def seeing_thing():
884         if t["fovmap"] and ("a" == filter or "f" == filter):
885             for id in world_db["Things"]:
886                 Thing = world_db["Things"][id]
887                 if Thing != t and Thing["T_LIFEPOINTS"] and \
888                    t["T_TYPE"] != Thing["T_TYPE"] and \
889                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
890                                           * world_db["MAP_LENGTH"])
891                                           + Thing["T_POSX"]]):
892                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
893                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
894                                           t["T_LIFEPOINTS"]) \
895                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
896                                              t["T_LIFEPOINTS"]):
897                         return True
898         elif t["T_MEMMAP"] and "c" == filter:
899             for mt in t["T_MEMTHING"]:
900                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
901                                          + mt[2]]) \
902                    and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
903                     return True
904         return False
905
906     def set_cells_passable_on_memmap_to_65534_on_scoremap():
907         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
908         # ord_dot = ord(".")
909         # memmap = t["T_MEMMAP"]
910         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
911         #            if ord_dot == memmap[i]]:
912         #     set_map_score(i, 65534) # i.e. 65535-1
913         map = c_pointer_to_bytearray(t["T_MEMMAP"])
914         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
915             raise RuntimeError("No score map allocated for "
916                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
917
918     def init_score_map():
919         test = libpr.init_score_map()
920         if test:
921             raise RuntimeError("Malloc error in init_score_map().")
922         ord_v = ord("v")
923         ord_blank = ord(" ")
924         set_cells_passable_on_memmap_to_65534_on_scoremap()
925         if "a" == filter:
926             for id in world_db["Things"]:
927                 Thing = world_db["Things"][id]
928                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
929                       + Thing["T_POSX"]
930                 if t != Thing and Thing["T_LIFEPOINTS"] and \
931                    t["T_TYPE"] != Thing["T_TYPE"] and \
932                    ord_v == t["fovmap"][pos] and \
933                    t["T_LIFEPOINTS"] > \
934                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
935                     set_map_score(pos, 0)
936                 elif t["T_TYPE"] == Thing["T_TYPE"]:
937                     set_map_score(pos, 65535)
938         elif "f" == filter:
939             for id in [id for id in world_db["Things"]
940                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
941                 Thing = world_db["Things"][id]
942                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
943                       + Thing["T_POSX"]
944                 if t["T_TYPE"] != Thing["T_TYPE"] and \
945                    ord_v == t["fovmap"][pos] and \
946                    t["T_LIFEPOINTS"] <= \
947                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
948                     set_map_score(pos, 0)
949         elif "c" == filter:
950             for mt in [mt for mt in t["T_MEMTHING"]
951                        if ord_blank != t["T_MEMMAP"][mt[1]
952                                                     * world_db["MAP_LENGTH"]
953                                                     + mt[2]]
954                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
955                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
956         elif "s" == filter:
957             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
958
959     def rand_target_dir(neighbors, cmp, dirs):
960         candidates = []
961         n_candidates = 0
962         for i in range(len(dirs)):
963             if cmp == neighbors[i]:
964                 candidates.append(dirs[i])
965                 n_candidates += 1
966         return candidates[rand.next() % n_candidates] if n_candidates else 0
967
968     def get_neighbor_scores(dirs, eye_pos):
969         scores = []
970         if libpr.ready_neighbor_scores(eye_pos):
971             raise RuntimeError("No score map allocated for " +
972                                "ready_neighbor_scores.()")
973         for i in range(len(dirs)):
974             scores.append(libpr.get_neighbor_score(i))
975         return scores
976
977     def get_dir_from_neighbors():
978         dir_to_target = False
979         dirs = "edcxsw"
980         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
981         neighbors = get_neighbor_scores(dirs, eye_pos)
982         if "f" == filter:
983             inhabited = [world_db["Things"][id]["T_POSY"]
984                          * world_db["MAP_LENGTH"]
985                          + world_db["Things"][id]["T_POSX"]
986                          for id in world_db["Things"]
987                          if world_db["Things"][id]["T_LIFEPOINTS"]]
988             for i in range(len(dirs)):
989                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
990                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
991                           + libpr.result_x()
992                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
993                     neighbors[i] = 65535
994                     break
995         minmax_start = 0 if "f" == filter else 65535 - 1
996         minmax_neighbor = minmax_start
997         for i in range(len(dirs)):
998             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
999                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1000                or ("f" != filter and minmax_neighbor > neighbors[i]):
1001                 minmax_neighbor = neighbors[i]
1002         if minmax_neighbor != minmax_start:
1003             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1004         if "f" == filter:
1005             if not dir_to_target:
1006                 if 1 == get_map_score(eye_pos):
1007                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
1008                 elif 3 >= get_map_score(eye_pos):
1009                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1010                                       if
1011                                       world_db["ThingActions"][id]["TA_NAME"]
1012                                          == "wait"][0]
1013                     return 1
1014             elif dir_to_target and 3 < get_map_score(eye_pos):
1015                 dir_to_target = 0
1016         elif "a" == filter and 10 <= get_map_score(eye_pos):
1017             dir_to_target = 0
1018         return dir_to_target
1019
1020     dir_to_target = False
1021     mem_depth_c = b' '
1022     run_i = 9 + 1 if "s" == filter else 1
1023     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1024         run_i -= 1
1025         init_score_map()
1026         mem_depth_c = b'9' if b' ' == mem_depth_c \
1027                            else bytes([mem_depth_c[0] - 1])
1028         if libpr.dijkstra_map():
1029             raise RuntimeError("No score map allocated for dijkstra_map().")
1030         dir_to_target = get_dir_from_neighbors()
1031         libpr.free_score_map()
1032         if dir_to_target and str == type(dir_to_target):
1033             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1034                               if world_db["ThingActions"][id]["TA_NAME"]
1035                                  == "move"][0]
1036             t["T_ARGUMENT"] = ord(dir_to_target)
1037     return dir_to_target
1038
1039
1040 def standing_on_consumable(t):
1041     """Return True/False whether t is standing on a consumable."""
1042     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1043                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1044                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1045                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1046                           ["TT_CONSUMABLE"]]:
1047         return True
1048     return False
1049
1050
1051 def get_inventory_slot_to_consume(t):
1052     """Return slot Id of strongest consumable in t's inventory, else -1."""
1053     cmp_consumability = 0
1054     selection = -1
1055     i = 0
1056     for id in t["T_CARRIES"]:
1057         type = world_db["Things"][id]["T_TYPE"]
1058         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
1059             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
1060             selection = i
1061         i += 1
1062     return selection
1063
1064
1065 def ai(t):
1066     """Determine next command/argment for actor t via AI algorithms.
1067
1068     AI will look for, and move towards, enemies (animate Things not of their
1069     own ThingType); if they see none, they will consume consumables in their
1070     inventory; if there are none, they will pick up what they stand on if they
1071     stand on consumables; if they stand on none, they will move towards the
1072     next consumable they see or remember on the map; if they see or remember
1073     none, they will explore parts of the map unseen since ever or for at least
1074     one turn; if there is nothing to explore, they will simply wait.
1075     """
1076     # # 7DRL add: Don't pick up or search things when inventory is full.
1077     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1078                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1079     if not get_dir_to_target(t, "f"):
1080         sel = get_inventory_slot_to_consume(t)
1081         if -1 != sel:
1082             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1083                               if world_db["ThingActions"][id]["TA_NAME"]
1084                                  == "use"][0]
1085             t["T_ARGUMENT"] = sel
1086         elif standing_on_consumable(t) \
1087              and (len(t["T_CARRIES"]) < # #
1088                  world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]): # #
1089             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1090                               if world_db["ThingActions"][id]["TA_NAME"]
1091                                  == "pick_up"][0]
1092         elif (not
1093               (len(t["T_CARRIES"]) < # #
1094                 world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"] # #
1095                and get_dir_to_target(t, "c"))) and \
1096              (not get_dir_to_target(t, "a")):
1097             get_dir_to_target(t, "s")
1098
1099
1100 def turn_over():
1101     """Run game world and its inhabitants until new player input expected."""
1102     id = 0
1103     whilebreaker = False
1104     while world_db["Things"][0]["T_LIFEPOINTS"]:
1105         proliferable_map = world_db["MAP"][:]
1106         for id in [id for id in world_db["Things"]
1107                    if not world_db["Things"][id]["carried"]]:
1108             y = world_db["Things"][id]["T_POSY"]
1109             x = world_db["Things"][id]["T_POSX"]
1110             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1111         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1112             if not id in world_db["Things"] or \
1113                world_db["Things"][id]["carried"]:   # May have been consumed or
1114                 continue                            # picked up during turn …
1115             Thing = world_db["Things"][id]
1116             if Thing["T_LIFEPOINTS"]:
1117                 if not Thing["T_COMMAND"]:
1118                     update_map_memory(Thing)
1119                     if 0 == id:
1120                         whilebreaker = True
1121                         break
1122                     ai(Thing)
1123                 try_healing(Thing)
1124                 Thing["T_PROGRESS"] += 1
1125                 taid = [a for a in world_db["ThingActions"]
1126                           if a == Thing["T_COMMAND"]][0]
1127                 ThingAction = world_db["ThingActions"][taid]
1128                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1129                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
1130                     Thing["T_COMMAND"] = 0
1131                     Thing["T_PROGRESS"] = 0
1132                 hunger(Thing)
1133             thingproliferation(Thing, proliferable_map)
1134         if whilebreaker:
1135             break
1136         world_db["TURN"] += 1
1137
1138
1139 def new_Thing(type, pos=(0, 0)):
1140     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1141     thing = {
1142         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1143         "T_ARGUMENT": 0,
1144         "T_PROGRESS": 0,
1145         "T_SATIATION": 0,
1146         "T_COMMAND": 0,
1147         "T_PLAYERDROP": 0,  # #
1148         "T_TYPE": type,
1149         "T_POSY": pos[0],
1150         "T_POSX": pos[1],
1151         "T_CARRIES": [],
1152         "carried": False,
1153         "T_MEMTHING": [],
1154         "T_MEMMAP": False,
1155         "T_MEMDEPTHMAP": False,
1156         "fovmap": False
1157     }
1158     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1159         build_fov_map(thing)
1160     return thing
1161
1162
1163 def id_setter(id, category, id_store=False, start_at_1=False):
1164     """Set ID of object of category to manipulate ID unused? Create new one.
1165
1166     The ID is stored as id_store.id (if id_store is set). If the integer of the
1167     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1168     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1169     always returned when no new object is created, otherwise the new object's
1170     ID.
1171     """
1172     min = 0 if start_at_1 else -1
1173     if str == type(id):
1174         id = integer_test(id, min)
1175     if None != id:
1176         if id in world_db[category]:
1177             if id_store:
1178                 id_store.id = id
1179             return None
1180         else:
1181             if (start_at_1 and 0 == id) \
1182                or ((not start_at_1) and (id < 0)):
1183                 id = 0 if start_at_1 else -1
1184                 while 1:
1185                     id = id + 1
1186                     if id not in world_db[category]:
1187                         break
1188             if id_store:
1189                 id_store.id = id
1190     return id
1191
1192
1193 def command_ping():
1194     """Send PONG line to server output file."""
1195     strong_write(io_db["file_out"], "PONG\n")
1196
1197
1198 def command_quit():
1199     """Abort server process."""
1200     if None == opts.replay:
1201         if world_db["WORLD_ACTIVE"]:
1202             save_world()
1203         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1204     raise SystemExit("received QUIT command")
1205
1206
1207 def command_thingshere(str_y, str_x):
1208     """Write to out file list of Things known to player at coordinate y, x."""
1209     if world_db["WORLD_ACTIVE"]:
1210         y = integer_test(str_y, 0, 255)
1211         x = integer_test(str_x, 0, 255)
1212         length = world_db["MAP_LENGTH"]
1213         if None != y and None != x and y < length and x < length:
1214             pos = (y * world_db["MAP_LENGTH"]) + x
1215             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1216             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1217                 for id in world_db["Things"]:
1218                     if y == world_db["Things"][id]["T_POSY"] \
1219                        and x == world_db["Things"][id]["T_POSX"] \
1220                        and not world_db["Things"][id]["carried"]:
1221                         type = world_db["Things"][id]["T_TYPE"]
1222                         name = world_db["ThingTypes"][type]["TT_NAME"]
1223                         strong_write(io_db["file_out"], name + "\n")
1224             else:
1225                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1226                     if y == mt[1] and x == mt[2]:
1227                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1228                         strong_write(io_db["file_out"], name + "\n")
1229             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1230         else:
1231             print("Ignoring: Invalid map coordinates.")
1232     else:
1233         print("Ignoring: Command only works on existing worlds.")
1234
1235
1236 def play_commander(action, args=False):
1237     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1238
1239     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1240     """
1241
1242     def set_command():
1243         id = [x for x in world_db["ThingActions"]
1244                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1245         world_db["Things"][0]["T_COMMAND"] = id
1246         turn_over()
1247
1248     def set_command_and_argument_int(str_arg):
1249         val = integer_test(str_arg, 0, 255)
1250         if None != val:
1251             world_db["Things"][0]["T_ARGUMENT"] = val
1252             set_command()
1253
1254     def set_command_and_argument_movestring(str_arg):
1255         if str_arg in directions_db:
1256             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1257             set_command()
1258         else:
1259             print("Ignoring: Argument must be valid direction string.")
1260
1261     if action == "move":
1262         return set_command_and_argument_movestring
1263     elif args:
1264         return set_command_and_argument_int
1265     else:
1266         return set_command
1267
1268
1269 def command_seedrandomness(seed_string):
1270     """Set rand seed to int(seed_string)."""
1271     val = integer_test(seed_string, 0, 4294967295)
1272     if None != val:
1273         rand.seed = val
1274
1275
1276 def command_makeworld(seed_string):
1277     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1278
1279     Seed rand with seed. Do more only with a "wait" ThingAction and
1280     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1281     world_db["Things"] emptied, call make_map() and set
1282     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1283     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1284     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1285     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1286     """
1287
1288     # def free_pos(plant=False):
1289     def free_pos(plant=False):  # #
1290         i = 0
1291         while 1:
1292             err = "Space to put thing on too hard to find. Map too small?"
1293             while 1:
1294                 y = rand.next() % world_db["MAP_LENGTH"]
1295                 x = rand.next() % world_db["MAP_LENGTH"]
1296                 pos = y * world_db["MAP_LENGTH"] + x;
1297                 if (not plant  # #
1298                     and "." == chr(world_db["MAP"][pos])) \
1299                    or ":" == chr(world_db["MAP"][pos]):  # #
1300                     break
1301                 i += 1
1302                 if i == 65535:
1303                     raise SystemExit(err)
1304             # Replica of C code, wrongly ignores animatedness of new Thing.
1305             pos_clear = (0 == len([id for id in world_db["Things"]
1306                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1307                                    if world_db["Things"][id]["T_POSY"] == y
1308                                    if world_db["Things"][id]["T_POSX"] == x]))
1309             if pos_clear:
1310                 break
1311         return (y, x)
1312
1313     val = integer_test(seed_string, 0, 4294967295)
1314     if None == val:
1315         return
1316     rand.seed = val
1317     player_will_be_generated = False
1318     playertype = world_db["PLAYER_TYPE"]
1319     for ThingType in world_db["ThingTypes"]:
1320         if playertype == ThingType:
1321             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1322                 player_will_be_generated = True
1323             break
1324     if not player_will_be_generated:
1325         print("Ignoring: No player type with start number >0 defined.")
1326         return
1327     wait_action = False
1328     for ThingAction in world_db["ThingActions"]:
1329         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1330             wait_action = True
1331     if not wait_action:
1332         print("Ignoring: No thing action with name 'wait' defined.")
1333         return
1334     world_db["Things"] = {}
1335     make_map()
1336     world_db["WORLD_ACTIVE"] = 1
1337     world_db["TURN"] = 1
1338     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1339         id = id_setter(-1, "Things")
1340         world_db["Things"][id] = new_Thing(playertype, free_pos())
1341     if not world_db["Things"][0]["fovmap"]:
1342         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1343         world_db["Things"][0]["fovmap"] = empty_fovmap
1344     update_map_memory(world_db["Things"][0])
1345     for type in world_db["ThingTypes"]:
1346         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1347             if type != playertype:
1348                 id = id_setter(-1, "Things")
1349                 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"]  # #
1350                 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1351     strong_write(io_db["file_out"], "NEW_WORLD\n")
1352
1353
1354 def command_maplength(maplength_string):
1355     """Redefine map length. Invalidate map, therefore lose all things on it."""
1356     val = integer_test(maplength_string, 1, 256)
1357     if None != val:
1358         world_db["MAP_LENGTH"] = val
1359         world_db["MAP"] = False
1360         set_world_inactive()
1361         world_db["Things"] = {}
1362         libpr.set_maplength(val)
1363
1364
1365 def command_worldactive(worldactive_string):
1366     """Toggle world_db["WORLD_ACTIVE"] if possible.
1367
1368     An active world can always be set inactive. An inactive world can only be
1369     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1370     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1371     """
1372     val = integer_test(worldactive_string, 0, 1)
1373     if val:
1374         if 0 != world_db["WORLD_ACTIVE"]:
1375             if 0 == val:
1376                 set_world_inactive()
1377             else:
1378                 print("World already active.")
1379         elif 0 == world_db["WORLD_ACTIVE"]:
1380             wait_exists = False
1381             for ThingAction in world_db["ThingActions"]:
1382                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1383                     wait_exists = True
1384                     break
1385             player_exists = False
1386             for Thing in world_db["Things"]:
1387                 if 0 == Thing:
1388                     player_exists = True
1389                     break
1390             if wait_exists and player_exists and world_db["MAP"]:
1391                 for id in world_db["Things"]:
1392                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1393                         build_fov_map(world_db["Things"][id])
1394                         if 0 == id:
1395                             update_map_memory(world_db["Things"][id], False)
1396                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1397                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1398                     world_db["Things"][0]["fovmap"] = empty_fovmap
1399                 world_db["WORLD_ACTIVE"] = 1
1400             else:
1401                 print("Ignoring: Not all conditions for world activation met.")
1402
1403
1404 def test_for_id_maker(object, category):
1405     """Return decorator testing for object having "id" attribute."""
1406     def decorator(f):
1407         def helper(*args):
1408             if hasattr(object, "id"):
1409                 f(*args)
1410             else:
1411                 print("Ignoring: No " + category +
1412                       " defined to manipulate yet.")
1413         return helper
1414     return decorator
1415
1416
1417 def command_tid(id_string):
1418     """Set ID of Thing to manipulate. ID unused? Create new one.
1419
1420     Default new Thing's type to the first available ThingType, others: zero.
1421     """
1422     id = id_setter(id_string, "Things", command_tid)
1423     if None != id:
1424         if world_db["ThingTypes"] == {}:
1425             print("Ignoring: No ThingType to settle new Thing in.")
1426             return
1427         type = list(world_db["ThingTypes"].keys())[0]
1428         world_db["Things"][id] = new_Thing(type)
1429
1430
1431 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1432
1433
1434 @test_Thing_id
1435 def command_tcommand(str_int):
1436     """Set T_COMMAND of selected Thing."""
1437     val = integer_test(str_int, 0)
1438     if None != val:
1439         if 0 == val or val in world_db["ThingActions"]:
1440             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1441         else:
1442             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1443
1444
1445 @test_Thing_id
1446 def command_ttype(str_int):
1447     """Set T_TYPE of selected Thing."""
1448     val = integer_test(str_int, 0)
1449     if None != val:
1450         if val in world_db["ThingTypes"]:
1451             world_db["Things"][command_tid.id]["T_TYPE"] = val
1452         else:
1453             print("Ignoring: ThingType ID belongs to no known ThingType.")
1454
1455
1456 @test_Thing_id
1457 def command_tcarries(str_int):
1458     """Append int(str_int) to T_CARRIES of selected Thing.
1459
1460     The ID int(str_int) must not be of the selected Thing, and must belong to a
1461     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1462     """
1463     val = integer_test(str_int, 0)
1464     if None != val:
1465         if val == command_tid.id:
1466             print("Ignoring: Thing cannot carry itself.")
1467         elif val in world_db["Things"] \
1468              and not world_db["Things"][val]["carried"]:
1469             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1470             world_db["Things"][val]["carried"] = True
1471         else:
1472             print("Ignoring: Thing not available for carrying.")
1473     # Note that the whole carrying structure is different from the C version:
1474     # Carried-ness is marked by a "carried" flag, not by Things containing
1475     # Things internally.
1476
1477
1478 @test_Thing_id
1479 def command_tmemthing(str_t, str_y, str_x):
1480     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1481
1482     The type must fit to an existing ThingType, and the position into the map.
1483     """
1484     type = integer_test(str_t, 0)
1485     posy = integer_test(str_y, 0, 255)
1486     posx = integer_test(str_x, 0, 255)
1487     if None != type and None != posy and None != posx:
1488         if type not in world_db["ThingTypes"] \
1489            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1490             print("Ignoring: Illegal value for thing type or position.")
1491         else:
1492             memthing = (type, posy, posx)
1493             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1494
1495
1496 def setter_map(maptype):
1497     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1498
1499     If no map of maptype exists yet, initialize it with ' ' bytes first.
1500     """
1501
1502     def valid_map_line(str_int, mapline):
1503         val = integer_test(str_int, 0, 255)
1504         if None != val:
1505             if val >= world_db["MAP_LENGTH"]:
1506                 print("Illegal value for map line number.")
1507             elif len(mapline) != world_db["MAP_LENGTH"]:
1508                 print("Map line length is unequal map width.")
1509             else:
1510                 return val
1511         return None
1512
1513     def nonThingMap_helper(str_int, mapline):
1514         val = valid_map_line(str_int, mapline)
1515         if None != val:
1516             length = world_db["MAP_LENGTH"]
1517             if not world_db["MAP"]:
1518                 map = bytearray(b' ' * (length ** 2))
1519             else:
1520                 map = world_db["MAP"]
1521             map[val * length:(val * length) + length] = mapline.encode()
1522             if not world_db["MAP"]:
1523                 world_db["MAP"] = map
1524
1525     @test_Thing_id
1526     def ThingMap_helper(str_int, mapline):
1527         val = valid_map_line(str_int, mapline)
1528         if None != val:
1529             length = world_db["MAP_LENGTH"]
1530             if not world_db["Things"][command_tid.id][maptype]:
1531                 map = bytearray(b' ' * (length ** 2))
1532             else:
1533                 map = world_db["Things"][command_tid.id][maptype]
1534             map[val * length:(val * length) + length] = mapline.encode()
1535             if not world_db["Things"][command_tid.id][maptype]:
1536                 world_db["Things"][command_tid.id][maptype] = map
1537
1538     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1539
1540
1541 def setter_tpos(axis):
1542     """Generate setter for T_POSX or  T_POSY of selected Thing.
1543
1544     If world is active, rebuilds animate things' fovmap, player's memory map.
1545     """
1546     @test_Thing_id
1547     def helper(str_int):
1548         val = integer_test(str_int, 0, 255)
1549         if None != val:
1550             if val < world_db["MAP_LENGTH"]:
1551                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1552                 if world_db["WORLD_ACTIVE"] \
1553                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1554                     build_fov_map(world_db["Things"][command_tid.id])
1555                     if 0 == command_tid.id:
1556                         update_map_memory(world_db["Things"][command_tid.id])
1557             else:
1558                 print("Ignoring: Position is outside of map.")
1559     return helper
1560
1561
1562 def command_ttid(id_string):
1563     """Set ID of ThingType to manipulate. ID unused? Create new one.
1564
1565     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1566     """
1567     id = id_setter(id_string, "ThingTypes", command_ttid)
1568     if None != id:
1569         world_db["ThingTypes"][id] = {
1570             "TT_NAME": "(none)",
1571             "TT_CONSUMABLE": 0,
1572             "TT_LIFEPOINTS": 0,
1573             "TT_PROLIFERATE": 0,
1574             "TT_START_NUMBER": 0,
1575             "TT_STORAGE": 0, # #
1576             "TT_SYMBOL": "?",
1577             "TT_CORPSE_ID": id
1578         }
1579
1580
1581 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1582
1583
1584 @test_ThingType_id
1585 def command_ttname(name):
1586     """Set TT_NAME of selected ThingType."""
1587     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1588
1589
1590 @test_ThingType_id
1591 def command_ttsymbol(char):
1592     """Set TT_SYMBOL of selected ThingType. """
1593     if 1 == len(char):
1594         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1595     else:
1596         print("Ignoring: Argument must be single character.")
1597
1598
1599 @test_ThingType_id
1600 def command_ttcorpseid(str_int):
1601     """Set TT_CORPSE_ID of selected ThingType."""
1602     val = integer_test(str_int, 0)
1603     if None != val:
1604         if val in world_db["ThingTypes"]:
1605             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1606         else:
1607             print("Ignoring: Corpse ID belongs to no known ThignType.")
1608
1609
1610 def command_taid(id_string):
1611     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1612
1613     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1614     """
1615     id = id_setter(id_string, "ThingActions", command_taid, True)
1616     if None != id:
1617         world_db["ThingActions"][id] = {
1618             "TA_EFFORT": 1,
1619             "TA_NAME": "wait"
1620         }
1621
1622
1623 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1624
1625
1626 @test_ThingAction_id
1627 def command_taname(name):
1628     """Set TA_NAME of selected ThingAction.
1629
1630     The name must match a valid thing action function. If after the name
1631     setting no ThingAction with name "wait" remains, call set_world_inactive().
1632     """
1633     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1634        or name == "pick_up":
1635         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1636         if 1 == world_db["WORLD_ACTIVE"]:
1637             wait_defined = False
1638             for id in world_db["ThingActions"]:
1639                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1640                     wait_defined = True
1641                     break
1642             if not wait_defined:
1643                 set_world_inactive()
1644     else:
1645         print("Ignoring: Invalid action name.")
1646     # In contrast to the original,naming won't map a function to a ThingAction.
1647
1648
1649 def command_ai():
1650     """Call ai() on player Thing, then turn_over()."""
1651     ai(world_db["Things"][0])
1652     turn_over()
1653
1654
1655 """Commands database.
1656
1657 Map command start tokens to ([0]) number of expected command arguments, ([1])
1658 the command's meta-ness (i.e. is it to be written to the record file, is it to
1659 be ignored in replay mode if read from server input file), and ([2]) a function
1660 to be called on it.
1661 """
1662 commands_db = {
1663     "QUIT": (0, True, command_quit),
1664     "PING": (0, True, command_ping),
1665     "THINGS_HERE": (2, True, command_thingshere),
1666     "MAKE_WORLD": (1, False, command_makeworld),
1667     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1668     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1669     "GOD_MOOD": (1, False, setter(None, "GOD_MOOD", -32768, 32767)),  # #
1670     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
1671     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1672     "MAP_LENGTH": (1, False, command_maplength),
1673     "WORLD_ACTIVE": (1, False, command_worldactive),
1674     "MAP": (2, False, setter_map("MAP")),
1675     "TA_ID": (1, False, command_taid),
1676     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1677     "TA_NAME": (1, False, command_taname),
1678     "TT_ID": (1, False, command_ttid),
1679     "TT_NAME": (1, False, command_ttname),
1680     "TT_SYMBOL": (1, False, command_ttsymbol),
1681     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1682     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1683                                        0, 65535)),
1684     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1685                                          0, 255)),
1686     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1687                                         0, 255)),
1688     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1689     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)),  # #
1690     "T_ID": (1, False, command_tid),
1691     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1692     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1693     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1694     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1695     "T_COMMAND": (1, False, command_tcommand),
1696     "T_TYPE": (1, False, command_ttype),
1697     "T_CARRIES": (1, False, command_tcarries),
1698     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1699     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1700     "T_MEMTHING": (3, False, command_tmemthing),
1701     "T_POSY": (1, False, setter_tpos("Y")),
1702     "T_POSX": (1, False, setter_tpos("X")),
1703     "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)),  # #
1704     "wait": (0, False, play_commander("wait")),
1705     "move": (1, False, play_commander("move")),
1706     "pick_up": (0, False, play_commander("pick_up")),
1707     "drop": (1, False, play_commander("drop", True)),
1708     "use": (1, False, play_commander("use", True)),
1709     "ai": (0, False, command_ai)
1710 }
1711 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1712
1713
1714 """World state database. With sane default values. (Randomness is in rand.)"""
1715 world_db = {
1716     "TURN": 0,
1717     "MAP_LENGTH": 64,
1718     "PLAYER_TYPE": 0,
1719     "WORLD_ACTIVE": 0,
1720     "GOD_MOOD": 0,  # #
1721     "GOD_FAVOR": 0,  # #
1722     "MAP": False,
1723     "ThingActions": {},
1724     "ThingTypes": {},
1725     "Things": {}
1726 }
1727
1728 """Mapping of direction names to internal direction chars."""
1729 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1730                  "west": "s", "north-west": "w", "north-east": "e"}
1731
1732 """File IO database."""
1733 io_db = {
1734     "path_save": "save",
1735     "path_record": "record_save",
1736     "path_worldconf": "confserver/world",
1737     "path_server": "server/",
1738     "path_in": "server/in",
1739     "path_out": "server/out",
1740     "path_worldstate": "server/worldstate",
1741     "tmp_suffix": "_tmp",
1742     "kicked_by_rival": False,
1743     "worldstate_updateable": False
1744 }
1745
1746
1747 try:
1748     libpr = prep_library()
1749     rand = RandomnessIO()
1750     opts = parse_command_line_arguments()
1751     if opts.savefile:
1752         io_db["path_save"] = opts.savefile
1753         io_db["path_record"] = "record_" + opts.savefile
1754     setup_server_io()
1755     if opts.verbose:
1756         io_db["verbose"] = True
1757     if None != opts.replay:
1758         replay_game()
1759     else:
1760         play_game()
1761 except SystemExit as exit:
1762     print("ABORTING: " + exit.args[0])
1763 except:
1764     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1765     raise
1766 finally:
1767     cleanup_server_io()