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