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