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