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