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