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