home · contact · privacy
Server/py: Optimize update_map_memory().
[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         for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
846                   if '.' == chr(t["T_MEMMAP"][i])]:
847             set_map_score(i, 65535 - 1)
848         if "a" == filter:
849             for id in world_db["Things"]:
850                 Thing = world_db["Things"][id]
851                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
852                       + Thing["T_POSX"]
853                 if t != Thing and Thing["T_LIFEPOINTS"] and \
854                    t["T_TYPE"] != Thing["T_TYPE"] and \
855                    'v' == chr(t["fovmap"][pos]) and \
856                    t["T_LIFEPOINTS"] > \
857                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
858                     set_map_score(pos, 0)
859                 elif t["T_TYPE"] == Thing["T_TYPE"]:
860                     set_map_score(pos, 65535)
861         elif "f" == filter:
862             for id in [id for id in world_db["Things"]
863                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
864                 Thing = world_db["Things"][id]
865                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
866                       + Thing["T_POSX"]
867                 if t["T_TYPE"] != Thing["T_TYPE"] and \
868                    'v' == chr(t["fovmap"][pos]) and \
869                    t["T_LIFEPOINTS"] <= \
870                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
871                     set_map_score(pos, 0)
872         elif "c" == filter:
873             for mt in [mt for mt in t["T_MEMTHING"]
874                        if ' ' != chr(t["T_MEMMAP"][mt[1]
875                                                    * world_db["MAP_LENGTH"]
876                                                    + mt[2]])
877                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
878                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
879         elif "s" == filter:
880             for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
881                       if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
882                 set_map_score(i, 0)
883
884     def rand_target_dir(neighbors, cmp, dirs):
885         candidates = []
886         n_candidates = 0
887         for i in range(len(dirs)):
888             if cmp == neighbors[i]:
889                 candidates.append(dirs[i])
890                 n_candidates += 1
891         return candidates[rand.next() % n_candidates] if n_candidates else 0
892
893     def get_neighbor_scores(dirs, eye_pos):
894         scores = []
895         if libpr.ready_neighbor_scores(eye_pos):
896             raise RuntimeError("No score map allocated for " +
897                                "ready_neighbor_scores.()")
898         for i in range(len(dirs)):
899             scores.append(libpr.get_neighbor_score(i))
900         return scores
901
902     def get_dir_from_neighbors():
903         dir_to_target = False
904         dirs = "edcxsw"
905         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
906         neighbors = get_neighbor_scores(dirs, eye_pos)
907         if "f" == filter:
908             inhabited = [world_db["Things"][id]["T_POSY"]
909                          * world_db["MAP_LENGTH"]
910                          + world_db["Things"][id]["T_POSX"]
911                          for id in world_db["Things"]
912                          if world_db["Things"][id]["T_LIFEPOINTS"]]
913             for i in range(len(dirs)):
914                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
915                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
916                           + libpr.result_x()
917                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
918                     neighbors[i] = 65535
919                     break
920         minmax_start = 0 if "f" == filter else 65535 - 1
921         minmax_neighbor = minmax_start
922         for i in range(len(dirs)):
923             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
924                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
925                or ("f" != filter and minmax_neighbor > neighbors[i]):
926                 minmax_neighbor = neighbors[i]
927         if minmax_neighbor != minmax_start:
928             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
929         if "f" == filter:
930             if not dir_to_target:
931                 if 1 == get_map_score(eye_pos):
932                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
933                 elif 3 >= get_map_score(eye_pos):
934                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
935                                       if
936                                       world_db["ThingActions"][id]["TA_NAME"]
937                                          == "wait"][0]
938                     return 1
939             elif dir_to_target and 3 < get_map_score(eye_pos):
940                 dir_to_target = 0
941         elif "a" == filter and 10 <= get_map_score(eye_pos):
942             dir_to_target = 0
943         return dir_to_target
944
945     dir_to_target = False
946     mem_depth_c = b' '
947     run_i = 9 + 1 if "s" == filter else 1
948     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
949         run_i -= 1
950         init_score_map()
951         mem_depth_c = b'9' if b' ' == mem_depth_c \
952                            else bytes([mem_depth_c[0] - 1])
953         if libpr.dijkstra_map():
954             raise RuntimeError("No score map allocated for dijkstra_map().")
955         dir_to_target = get_dir_from_neighbors()
956         libpr.free_score_map()
957         if dir_to_target and str == type(dir_to_target):
958             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
959                               if world_db["ThingActions"][id]["TA_NAME"]
960                                  == "move"][0]
961             t["T_ARGUMENT"] = ord(dir_to_target)
962     return dir_to_target
963
964
965 def standing_on_consumable(t):
966     """Return True/False whether t is standing on a consumable."""
967     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
968                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
969                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
970                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
971                           ["TT_CONSUMABLE"]]:
972         return True
973     return False
974
975
976 def get_inventory_slot_to_consume(t):
977     """Return slot Id of strongest consumable in t's inventory, else -1."""
978     cmp_consumability = 0
979     selection = -1
980     i = 0
981     for id in t["T_CARRIES"]:
982         type = world_db["Things"][id]["T_TYPE"]
983         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
984             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
985             selection = i
986         i += 1
987     return selection
988
989
990 def ai(t):
991     """Determine next command/argment for actor t via AI algorithms.
992
993     AI will look for, and move towards, enemies (animate Things not of their
994     own ThingType); if they see none, they will consume consumables in their
995     inventory; if there are none, they will pick up what they stand on if they
996     stand on consumables; if they stand on none, they will move towards the
997     next consumable they see or remember on the map; if they see or remember
998     none, they will explore parts of the map unseen since ever or for at least
999     one turn; if there is nothing to explore, they will simply wait.
1000     """
1001     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1002                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1003     if not get_dir_to_target(t, "f"):
1004         sel = get_inventory_slot_to_consume(t)
1005         if -1 != sel:
1006             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1007                               if world_db["ThingActions"][id]["TA_NAME"]
1008                                  == "use"][0]
1009             t["T_ARGUMENT"] = sel
1010         elif standing_on_consumable(t):
1011             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1012                               if world_db["ThingActions"][id]["TA_NAME"]
1013                                  == "pick_up"][0]
1014         elif (not get_dir_to_target(t, "c")) and \
1015              (not get_dir_to_target(t, "a")):
1016             get_dir_to_target(t, "s")
1017
1018
1019 def turn_over():
1020     """Run game world and its inhabitants until new player input expected."""
1021     id = 0
1022     whilebreaker = False
1023     while world_db["Things"][0]["T_LIFEPOINTS"]:
1024         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1025             if not id in world_db["Things"] or \
1026                world_db["Things"][id]["carried"]:   # May have been consumed or
1027                 continue                            # picked up during turn …
1028             Thing = world_db["Things"][id]
1029             if Thing["T_LIFEPOINTS"]:
1030                 if not Thing["T_COMMAND"]:
1031                     update_map_memory(Thing)
1032                     if 0 == id:
1033                         whilebreaker = True
1034                         break
1035                     ai(Thing)
1036                 try_healing(Thing)
1037                 Thing["T_PROGRESS"] += 1
1038                 taid = [a for a in world_db["ThingActions"]
1039                           if a == Thing["T_COMMAND"]][0]
1040                 ThingAction = world_db["ThingActions"][taid]
1041                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1042                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
1043                     Thing["T_COMMAND"] = 0
1044                     Thing["T_PROGRESS"] = 0
1045                 hunger(Thing)
1046             thingproliferation(Thing)
1047         if whilebreaker:
1048             break
1049         world_db["TURN"] += 1
1050
1051
1052 def new_Thing(type, pos=(0, 0)):
1053     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1054     thing = {
1055         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1056         "T_ARGUMENT": 0,
1057         "T_PROGRESS": 0,
1058         "T_SATIATION": 0,
1059         "T_COMMAND": 0,
1060         "T_TYPE": type,
1061         "T_POSY": pos[0],
1062         "T_POSX": pos[1],
1063         "T_CARRIES": [],
1064         "carried": False,
1065         "T_MEMTHING": [],
1066         "T_MEMMAP": False,
1067         "T_MEMDEPTHMAP": False,
1068         "fovmap": False
1069     }
1070     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1071         build_fov_map(thing)
1072     return thing
1073
1074
1075 def id_setter(id, category, id_store=False, start_at_1=False):
1076     """Set ID of object of category to manipulate ID unused? Create new one.
1077
1078     The ID is stored as id_store.id (if id_store is set). If the integer of the
1079     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
1080     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
1081     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
1082     new object is created, otherwise the new object's ID.
1083     """
1084     min = 0 if start_at_1 else -32768
1085     max = 255 if start_at_1 else 32767
1086     if str == type(id):
1087         id = integer_test(id, min, max)
1088     if None != id:
1089         if id in world_db[category]:
1090             if id_store:
1091                 id_store.id = id
1092             return None
1093         else:
1094             if (start_at_1 and 0 == id) \
1095                or ((not start_at_1) and (id < 0 or id > 255)):
1096                 id = -1
1097                 while 1:
1098                     id = id + 1
1099                     if id not in world_db[category]:
1100                         break
1101                 if id > 255:
1102                     print("Ignoring: "
1103                           "No unused ID available to add to ID list.")
1104                     return None
1105             if id_store:
1106                 id_store.id = id
1107     return id
1108
1109
1110 def command_ping():
1111     """Send PONG line to server output file."""
1112     strong_write(io_db["file_out"], "PONG\n")
1113
1114
1115 def command_quit():
1116     """Abort server process."""
1117     save_world()
1118     atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1119     raise SystemExit("received QUIT command")
1120
1121
1122 def command_thingshere(str_y, str_x):
1123     """Write to out file list of Things known to player at coordinate y, x."""
1124     if world_db["WORLD_ACTIVE"]:
1125         y = integer_test(str_y, 0, 255)
1126         x = integer_test(str_x, 0, 255)
1127         length = world_db["MAP_LENGTH"]
1128         if None != y and None != x and y < length and x < length:
1129             pos = (y * world_db["MAP_LENGTH"]) + x
1130             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1131             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1132                 for id in world_db["Things"]:
1133                     if y == world_db["Things"][id]["T_POSY"] \
1134                        and x == world_db["Things"][id]["T_POSX"] \
1135                        and not world_db["Things"][id]["carried"]:
1136                         type = world_db["Things"][id]["T_TYPE"]
1137                         name = world_db["ThingTypes"][type]["TT_NAME"]
1138                         strong_write(io_db["file_out"], name + "\n")
1139             else:
1140                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1141                     if y == mt[1] and x == mt[2]:
1142                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1143                         strong_write(io_db["file_out"], name + "\n")
1144             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1145         else:
1146             print("Ignoring: Invalid map coordinates.")
1147     else:
1148         print("Ignoring: Command only works on existing worlds.")
1149
1150
1151 def play_commander(action, args=False):
1152     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1153
1154     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1155     """
1156
1157     def set_command():
1158         id = [x for x in world_db["ThingActions"]
1159                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1160         world_db["Things"][0]["T_COMMAND"] = id
1161         turn_over()
1162
1163     def set_command_and_argument_int(str_arg):
1164         val = integer_test(str_arg, 0, 255)
1165         if None != val:
1166             world_db["Things"][0]["T_ARGUMENT"] = val
1167             set_command()
1168
1169     def set_command_and_argument_movestring(str_arg):
1170         if str_arg in directions_db:
1171             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1172             set_command()
1173         else:
1174             print("Ignoring: Argument must be valid direction string.")
1175
1176     if action == "move":
1177         return set_command_and_argument_movestring
1178     elif args:
1179         return set_command_and_argument_int
1180     else:
1181         return set_command
1182
1183
1184 def command_seedrandomness(seed_string):
1185     """Set rand seed to int(seed_string)."""
1186     val = integer_test(seed_string, 0, 4294967295)
1187     if None != val:
1188         rand.seed = val
1189
1190
1191 def command_seedmap(seed_string):
1192     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
1193     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
1194     remake_map()
1195
1196
1197 def command_makeworld(seed_string):
1198     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1199
1200     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
1201     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
1202     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
1203     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1204     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1205     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1206     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1207     """
1208
1209     def free_pos():
1210         i = 0
1211         while 1:
1212             err = "Space to put thing on too hard to find. Map too small?"
1213             while 1:
1214                 y = rand.next() % world_db["MAP_LENGTH"]
1215                 x = rand.next() % world_db["MAP_LENGTH"]
1216                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1217                     break
1218                 i += 1
1219                 if i == 65535:
1220                     raise SystemExit(err)
1221             # Replica of C code, wrongly ignores animatedness of new Thing.
1222             pos_clear = (0 == len([id for id in world_db["Things"]
1223                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1224                                    if world_db["Things"][id]["T_POSY"] == y
1225                                    if world_db["Things"][id]["T_POSX"] == x]))
1226             if pos_clear:
1227                 break
1228         return (y, x)
1229
1230     val = integer_test(seed_string, 0, 4294967295)
1231     if None == val:
1232         return
1233     rand.seed = val
1234     world_db["SEED_MAP"] = val
1235     player_will_be_generated = False
1236     playertype = world_db["PLAYER_TYPE"]
1237     for ThingType in world_db["ThingTypes"]:
1238         if playertype == ThingType:
1239             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1240                 player_will_be_generated = True
1241             break
1242     if not player_will_be_generated:
1243         print("Ignoring beyond SEED_MAP: " +
1244               "No player type with start number >0 defined.")
1245         return
1246     wait_action = False
1247     for ThingAction in world_db["ThingActions"]:
1248         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1249             wait_action = True
1250     if not wait_action:
1251         print("Ignoring beyond SEED_MAP: " +
1252               "No thing action with name 'wait' defined.")
1253         return
1254     world_db["Things"] = {}
1255     remake_map()
1256     world_db["WORLD_ACTIVE"] = 1
1257     world_db["TURN"] = 1
1258     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1259         id = id_setter(-1, "Things")
1260         world_db["Things"][id] = new_Thing(playertype, free_pos())
1261     update_map_memory(world_db["Things"][0])
1262     for type in world_db["ThingTypes"]:
1263         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1264             if type != playertype:
1265                 id = id_setter(-1, "Things")
1266                 world_db["Things"][id] = new_Thing(type, free_pos())
1267     strong_write(io_db["file_out"], "NEW_WORLD\n")
1268
1269
1270 def command_maplength(maplength_string):
1271     """Redefine map length. Invalidate map, therefore lose all things on it."""
1272     val = integer_test(maplength_string, 1, 256)
1273     if None != val:
1274         world_db["MAP_LENGTH"] = val
1275         set_world_inactive()
1276         world_db["Things"] = {}
1277         libpr.set_maplength(val)
1278
1279
1280 def command_worldactive(worldactive_string):
1281     """Toggle world_db["WORLD_ACTIVE"] if possible.
1282
1283     An active world can always be set inactive. An inactive world can only be
1284     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1285     activation, rebuild all Things' FOVs, and the player's map memory.
1286     """
1287     # In original version, map existence was also tested (unnecessarily?).
1288     val = integer_test(worldactive_string, 0, 1)
1289     if val:
1290         if 0 != world_db["WORLD_ACTIVE"]:
1291             if 0 == val:
1292                 set_world_inactive()
1293             else:
1294                 print("World already active.")
1295         elif 0 == world_db["WORLD_ACTIVE"]:
1296             wait_exists = False
1297             for ThingAction in world_db["ThingActions"]:
1298                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1299                     wait_exists = True
1300                     break
1301             player_exists = False
1302             for Thing in world_db["Things"]:
1303                 if 0 == Thing:
1304                     player_exists = True
1305                     break
1306             if wait_exists and player_exists:
1307                 for id in world_db["Things"]:
1308                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1309                         build_fov_map(world_db["Things"][id])
1310                         if 0 == id:
1311                             update_map_memory(world_db["Things"][id], False)
1312                 world_db["WORLD_ACTIVE"] = 1
1313
1314
1315 def test_for_id_maker(object, category):
1316     """Return decorator testing for object having "id" attribute."""
1317     def decorator(f):
1318         def helper(*args):
1319             if hasattr(object, "id"):
1320                 f(*args)
1321             else:
1322                 print("Ignoring: No " + category +
1323                       " defined to manipulate yet.")
1324         return helper
1325     return decorator
1326
1327
1328 def command_tid(id_string):
1329     """Set ID of Thing to manipulate. ID unused? Create new one.
1330
1331     Default new Thing's type to the first available ThingType, others: zero.
1332     """
1333     id = id_setter(id_string, "Things", command_tid)
1334     if None != id:
1335         if world_db["ThingTypes"] == {}:
1336             print("Ignoring: No ThingType to settle new Thing in.")
1337             return
1338         type = list(world_db["ThingTypes"].keys())[0]
1339         world_db["Things"][id] = new_Thing(type)
1340
1341
1342 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1343
1344
1345 @test_Thing_id
1346 def command_tcommand(str_int):
1347     """Set T_COMMAND of selected Thing."""
1348     val = integer_test(str_int, 0, 255)
1349     if None != val:
1350         if 0 == val or val in world_db["ThingActions"]:
1351             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1352         else:
1353             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1354
1355
1356 @test_Thing_id
1357 def command_ttype(str_int):
1358     """Set T_TYPE of selected Thing."""
1359     val = integer_test(str_int, 0, 255)
1360     if None != val:
1361         if val in world_db["ThingTypes"]:
1362             world_db["Things"][command_tid.id]["T_TYPE"] = val
1363         else:
1364             print("Ignoring: ThingType ID belongs to no known ThingType.")
1365
1366
1367 @test_Thing_id
1368 def command_tcarries(str_int):
1369     """Append int(str_int) to T_CARRIES of selected Thing.
1370
1371     The ID int(str_int) must not be of the selected Thing, and must belong to a
1372     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1373     """
1374     val = integer_test(str_int, 0, 255)
1375     if None != val:
1376         if val == command_tid.id:
1377             print("Ignoring: Thing cannot carry itself.")
1378         elif val in world_db["Things"] \
1379              and not world_db["Things"][val]["carried"]:
1380             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1381             world_db["Things"][val]["carried"] = True
1382         else:
1383             print("Ignoring: Thing not available for carrying.")
1384     # Note that the whole carrying structure is different from the C version:
1385     # Carried-ness is marked by a "carried" flag, not by Things containing
1386     # Things internally.
1387
1388
1389 @test_Thing_id
1390 def command_tmemthing(str_t, str_y, str_x):
1391     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1392
1393     The type must fit to an existing ThingType, and the position into the map.
1394     """
1395     type = integer_test(str_t, 0, 255)
1396     posy = integer_test(str_y, 0, 255)
1397     posx = integer_test(str_x, 0, 255)
1398     if None != type and None != posy and None != posx:
1399         if type not in world_db["ThingTypes"] \
1400            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1401             print("Ignoring: Illegal value for thing type or position.")
1402         else:
1403             memthing = (type, posy, posx)
1404             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1405
1406
1407 def setter_map(maptype):
1408     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1409
1410     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1411     """
1412     @test_Thing_id
1413     def helper(str_int, mapline):
1414         val = integer_test(str_int, 0, 255)
1415         if None != val:
1416             if val >= world_db["MAP_LENGTH"]:
1417                 print("Illegal value for map line number.")
1418             elif len(mapline) != world_db["MAP_LENGTH"]:
1419                 print("Map line length is unequal map width.")
1420             else:
1421                 length = world_db["MAP_LENGTH"]
1422                 map = None
1423                 if not world_db["Things"][command_tid.id][maptype]:
1424                     map = bytearray(b' ' * (length ** 2))
1425                 else:
1426                     map = world_db["Things"][command_tid.id][maptype]
1427                 map[val * length:(val * length) + length] = mapline.encode()
1428                 world_db["Things"][command_tid.id][maptype] = map
1429     return helper
1430
1431
1432 def setter_tpos(axis):
1433     """Generate setter for T_POSX or  T_POSY of selected Thing.
1434
1435     If world is active, rebuilds animate things' fovmap, player's memory map.
1436     """
1437     @test_Thing_id
1438     def helper(str_int):
1439         val = integer_test(str_int, 0, 255)
1440         if None != val:
1441             if val < world_db["MAP_LENGTH"]:
1442                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1443                 if world_db["WORLD_ACTIVE"] \
1444                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1445                     build_fov_map(world_db["Things"][command_tid.id])
1446                     if 0 == command_tid.id:
1447                         update_map_memory(world_db["Things"][command_tid.id])
1448             else:
1449                 print("Ignoring: Position is outside of map.")
1450     return helper
1451
1452
1453 def command_ttid(id_string):
1454     """Set ID of ThingType to manipulate. ID unused? Create new one.
1455
1456     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1457     """
1458     id = id_setter(id_string, "ThingTypes", command_ttid)
1459     if None != id:
1460         world_db["ThingTypes"][id] = {
1461             "TT_NAME": "(none)",
1462             "TT_CONSUMABLE": 0,
1463             "TT_LIFEPOINTS": 0,
1464             "TT_PROLIFERATE": 0,
1465             "TT_START_NUMBER": 0,
1466             "TT_SYMBOL": "?",
1467             "TT_CORPSE_ID": id
1468         }
1469
1470
1471 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1472
1473
1474 @test_ThingType_id
1475 def command_ttname(name):
1476     """Set TT_NAME of selected ThingType."""
1477     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1478
1479
1480 @test_ThingType_id
1481 def command_ttsymbol(char):
1482     """Set TT_SYMBOL of selected ThingType. """
1483     if 1 == len(char):
1484         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1485     else:
1486         print("Ignoring: Argument must be single character.")
1487
1488
1489 @test_ThingType_id
1490 def command_ttcorpseid(str_int):
1491     """Set TT_CORPSE_ID of selected ThingType."""
1492     val = integer_test(str_int, 0, 255)
1493     if None != val:
1494         if val in world_db["ThingTypes"]:
1495             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1496         else:
1497             print("Ignoring: Corpse ID belongs to no known ThignType.")
1498
1499
1500 def command_taid(id_string):
1501     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1502
1503     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1504     """
1505     id = id_setter(id_string, "ThingActions", command_taid, True)
1506     if None != id:
1507         world_db["ThingActions"][id] = {
1508             "TA_EFFORT": 1,
1509             "TA_NAME": "wait"
1510         }
1511
1512
1513 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1514
1515
1516 @test_ThingAction_id
1517 def command_taname(name):
1518     """Set TA_NAME of selected ThingAction.
1519
1520     The name must match a valid thing action function. If after the name
1521     setting no ThingAction with name "wait" remains, call set_world_inactive().
1522     """
1523     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1524        or name == "pick_up":
1525         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1526         if 1 == world_db["WORLD_ACTIVE"]:
1527             wait_defined = False
1528             for id in world_db["ThingActions"]:
1529                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1530                     wait_defined = True
1531                     break
1532             if not wait_defined:
1533                 set_world_inactive()
1534     else:
1535         print("Ignoring: Invalid action name.")
1536     # In contrast to the original,naming won't map a function to a ThingAction.
1537
1538
1539 def command_ai():
1540     """Call ai() on player Thing, then turn_over()."""
1541     ai(world_db["Things"][0])
1542     turn_over()
1543
1544
1545 """Commands database.
1546
1547 Map command start tokens to ([0]) number of expected command arguments, ([1])
1548 the command's meta-ness (i.e. is it to be written to the record file, is it to
1549 be ignored in replay mode if read from server input file), and ([2]) a function
1550 to be called on it.
1551 """
1552 commands_db = {
1553     "QUIT": (0, True, command_quit),
1554     "PING": (0, True, command_ping),
1555     "THINGS_HERE": (2, True, command_thingshere),
1556     "MAKE_WORLD": (1, False, command_makeworld),
1557     "SEED_MAP": (1, False, command_seedmap),
1558     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1559     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1560     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1561     "MAP_LENGTH": (1, False, command_maplength),
1562     "WORLD_ACTIVE": (1, False, command_worldactive),
1563     "TA_ID": (1, False, command_taid),
1564     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1565     "TA_NAME": (1, False, command_taname),
1566     "TT_ID": (1, False, command_ttid),
1567     "TT_NAME": (1, False, command_ttname),
1568     "TT_SYMBOL": (1, False, command_ttsymbol),
1569     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1570     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1571                                        0, 65535)),
1572     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1573                                          0, 255)),
1574     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1575                                         0, 255)),
1576     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1577     "T_ID": (1, False, command_tid),
1578     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1579     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1580     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1581     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1582     "T_COMMAND": (1, False, command_tcommand),
1583     "T_TYPE": (1, False, command_ttype),
1584     "T_CARRIES": (1, False, command_tcarries),
1585     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1586     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1587     "T_MEMTHING": (3, False, command_tmemthing),
1588     "T_POSY": (1, False, setter_tpos("Y")),
1589     "T_POSX": (1, False, setter_tpos("X")),
1590     "wait": (0, False, play_commander("wait")),
1591     "move": (1, False, play_commander("move")),
1592     "pick_up": (0, False, play_commander("pick_up")),
1593     "drop": (1, False, play_commander("drop", True)),
1594     "use": (1, False, play_commander("use", True)),
1595     "ai": (0, False, command_ai)
1596 }
1597
1598
1599 """World state database. With sane default values. (Randomness is in rand.)"""
1600 world_db = {
1601     "TURN": 0,
1602     "MAP_LENGTH": 64,
1603     "SEED_MAP": 0,
1604     "PLAYER_TYPE": 0,
1605     "WORLD_ACTIVE": 0,
1606     "ThingActions": {},
1607     "ThingTypes": {},
1608     "Things": {}
1609 }
1610
1611 """Mapping of direction names to internal direction chars."""
1612 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1613                  "west": "s", "north-west": "w", "north-east": "e"}
1614
1615 """File IO database."""
1616 io_db = {
1617     "path_save": "save",
1618     "path_record": "record_save",
1619     "path_worldconf": "confserver/world",
1620     "path_server": "server/",
1621     "path_in": "server/in",
1622     "path_out": "server/out",
1623     "path_worldstate": "server/worldstate",
1624     "tmp_suffix": "_tmp",
1625     "kicked_by_rival": False,
1626     "worldstate_updateable": False
1627 }
1628
1629
1630 try:
1631     libpr = prep_library()
1632     rand = RandomnessIO()
1633     opts = parse_command_line_arguments()
1634     if opts.savefile:
1635         io_db["path_save"] = opts.savefile
1636         io_db["path_record"] = "record_" + opts.savefile
1637     setup_server_io()
1638     if opts.verbose:
1639         io_db["verbose"] = True
1640     if None != opts.replay:
1641         replay_game()
1642     else:
1643         play_game()
1644 except SystemExit as exit:
1645     print("ABORTING: " + exit.args[0])
1646 except:
1647     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1648     raise
1649 finally:
1650     cleanup_server_io()