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