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