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