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