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