home · contact · privacy
Server/py: Add movable AI.
[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                     # write_thing_if_here()
1110                     if y == world_db["Things"][id]["T_POSY"] \
1111                        and x == world_db["Things"][id]["T_POSX"] \
1112                        and not world_db["Things"][id]["carried"]:
1113                         type = world_db["Things"][id]["T_TYPE"]
1114                         name = world_db["ThingTypes"][type]["TT_NAME"]
1115                         strong_write(io_db["file_out"], name + "\n")
1116             else:
1117                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1118                     if y == mt[1] and x == mt[2]:
1119                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1120                         strong_write(io_db["file_out"], name + "\n")
1121             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1122         else:
1123             print("Ignoring: Invalid map coordinates.")
1124     else:
1125         print("Ignoring: Command only works on existing worlds.")
1126
1127
1128 def play_commander(action, args=False):
1129     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1130
1131     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1132     """
1133
1134     def set_command():
1135         id = [x for x in world_db["ThingActions"]
1136                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1137         world_db["Things"][0]["T_COMMAND"] = id
1138         turn_over()
1139
1140     def set_command_and_argument_int(str_arg):
1141         val = integer_test(str_arg, 0, 255)
1142         if None != val:
1143             world_db["Things"][0]["T_ARGUMENT"] = val
1144             set_command()
1145
1146     def set_command_and_argument_movestring(str_arg):
1147         if str_arg in directions_db:
1148             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1149             set_command()
1150         else:
1151             print("Ignoring: Argument must be valid direction string.")
1152
1153     if action == "move":
1154         return set_command_and_argument_movestring
1155     elif args:
1156         return set_command_and_argument_int
1157     else:
1158         return set_command
1159
1160
1161 def command_seedrandomness(seed_string):
1162     """Set rand seed to int(seed_string)."""
1163     val = integer_test(seed_string, 0, 4294967295)
1164     if None != val:
1165         rand.seed = val
1166
1167
1168 def command_seedmap(seed_string):
1169     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
1170     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
1171     remake_map()
1172
1173
1174 def command_makeworld(seed_string):
1175     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1176
1177     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
1178     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
1179     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
1180     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1181     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1182     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1183     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1184     """
1185
1186     def free_pos():
1187         i = 0
1188         while 1:
1189             err = "Space to put thing on too hard to find. Map too small?"
1190             while 1:
1191                 y = rand.next() % world_db["MAP_LENGTH"]
1192                 x = rand.next() % world_db["MAP_LENGTH"]
1193                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1194                     break
1195                 i += 1
1196                 if i == 65535:
1197                     raise SystemExit(err)
1198             # Replica of C code, wrongly ignores animatedness of new Thing.
1199             pos_clear = (0 == len([id for id in world_db["Things"]
1200                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1201                                    if world_db["Things"][id]["T_POSY"] == y
1202                                    if world_db["Things"][id]["T_POSX"] == x]))
1203             if pos_clear:
1204                 break
1205         return (y, x)
1206
1207     val = integer_test(seed_string, 0, 4294967295)
1208     if None == val:
1209         return
1210     rand.seed = val
1211     world_db["SEED_MAP"] = val
1212     player_will_be_generated = False
1213     playertype = world_db["PLAYER_TYPE"]
1214     for ThingType in world_db["ThingTypes"]:
1215         if playertype == ThingType:
1216             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1217                 player_will_be_generated = True
1218             break
1219     if not player_will_be_generated:
1220         print("Ignoring beyond SEED_MAP: " +
1221               "No player type with start number >0 defined.")
1222         return
1223     wait_action = False
1224     for ThingAction in world_db["ThingActions"]:
1225         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1226             wait_action = True
1227     if not wait_action:
1228         print("Ignoring beyond SEED_MAP: " +
1229               "No thing action with name 'wait' defined.")
1230         return
1231     world_db["Things"] = {}
1232     remake_map()
1233     world_db["WORLD_ACTIVE"] = 1
1234     world_db["TURN"] = 1
1235     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1236         id = id_setter(-1, "Things")
1237         world_db["Things"][id] = new_Thing(playertype, free_pos())
1238     update_map_memory(world_db["Things"][0])
1239     for type in world_db["ThingTypes"]:
1240         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1241             if type != playertype:
1242                 id = id_setter(-1, "Things")
1243                 world_db["Things"][id] = new_Thing(type, free_pos())
1244     strong_write(io_db["file_out"], "NEW_WORLD\n")
1245
1246
1247 def command_maplength(maplength_string):
1248     """Redefine map length. Invalidate map, therefore lose all things on it."""
1249     val = integer_test(maplength_string, 1, 256)
1250     if None != val:
1251         world_db["MAP_LENGTH"] = val
1252         set_world_inactive()
1253         world_db["Things"] = {}
1254         libpr.set_maplength(val)
1255
1256
1257 def command_worldactive(worldactive_string):
1258     """Toggle world_db["WORLD_ACTIVE"] if possible.
1259
1260     An active world can always be set inactive. An inactive world can only be
1261     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1262     activation, rebuild all Things' FOVs, and the player's map memory.
1263     """
1264     # In original version, map existence was also tested (unnecessarily?).
1265     val = integer_test(worldactive_string, 0, 1)
1266     if val:
1267         if 0 != world_db["WORLD_ACTIVE"]:
1268             if 0 == val:
1269                 set_world_inactive()
1270             else:
1271                 print("World already active.")
1272         elif 0 == world_db["WORLD_ACTIVE"]:
1273             wait_exists = False
1274             for ThingAction in world_db["ThingActions"]:
1275                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1276                     wait_exists = True
1277                     break
1278             player_exists = False
1279             for Thing in world_db["Things"]:
1280                 if 0 == Thing:
1281                     player_exists = True
1282                     break
1283             if wait_exists and player_exists:
1284                 for id in world_db["Things"]:
1285                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1286                         build_fov_map(world_db["Things"][id])
1287                         if 0 == id:
1288                             update_map_memory(world_db["Things"][id])
1289                 world_db["WORLD_ACTIVE"] = 1
1290
1291
1292 def test_for_id_maker(object, category):
1293     """Return decorator testing for object having "id" attribute."""
1294     def decorator(f):
1295         def helper(*args):
1296             if hasattr(object, "id"):
1297                 f(*args)
1298             else:
1299                 print("Ignoring: No " + category +
1300                       " defined to manipulate yet.")
1301         return helper
1302     return decorator
1303
1304
1305 def command_tid(id_string):
1306     """Set ID of Thing to manipulate. ID unused? Create new one.
1307
1308     Default new Thing's type to the first available ThingType, others: zero.
1309     """
1310     id = id_setter(id_string, "Things", command_tid)
1311     if None != id:
1312         if world_db["ThingTypes"] == {}:
1313             print("Ignoring: No ThingType to settle new Thing in.")
1314             return
1315         type = list(world_db["ThingTypes"].keys())[0]
1316         world_db["Things"][id] = new_Thing(type)
1317
1318
1319 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1320
1321
1322 @test_Thing_id
1323 def command_tcommand(str_int):
1324     """Set T_COMMAND of selected Thing."""
1325     val = integer_test(str_int, 0, 255)
1326     if None != val:
1327         if 0 == val or val in world_db["ThingActions"]:
1328             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1329         else:
1330             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1331
1332
1333 @test_Thing_id
1334 def command_ttype(str_int):
1335     """Set T_TYPE of selected Thing."""
1336     val = integer_test(str_int, 0, 255)
1337     if None != val:
1338         if val in world_db["ThingTypes"]:
1339             world_db["Things"][command_tid.id]["T_TYPE"] = val
1340         else:
1341             print("Ignoring: ThingType ID belongs to no known ThingType.")
1342
1343
1344 @test_Thing_id
1345 def command_tcarries(str_int):
1346     """Append int(str_int) to T_CARRIES of selected Thing.
1347
1348     The ID int(str_int) must not be of the selected Thing, and must belong to a
1349     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1350     """
1351     val = integer_test(str_int, 0, 255)
1352     if None != val:
1353         if val == command_tid.id:
1354             print("Ignoring: Thing cannot carry itself.")
1355         elif val in world_db["Things"] \
1356              and not world_db["Things"][val]["carried"]:
1357             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1358             world_db["Things"][val]["carried"] = True
1359         else:
1360             print("Ignoring: Thing not available for carrying.")
1361     # Note that the whole carrying structure is different from the C version:
1362     # Carried-ness is marked by a "carried" flag, not by Things containing
1363     # Things internally.
1364
1365
1366 @test_Thing_id
1367 def command_tmemthing(str_t, str_y, str_x):
1368     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1369
1370     The type must fit to an existing ThingType, and the position into the map.
1371     """
1372     type = integer_test(str_t, 0, 255)
1373     posy = integer_test(str_y, 0, 255)
1374     posx = integer_test(str_x, 0, 255)
1375     if None != type and None != posy and None != posx:
1376         if type not in world_db["ThingTypes"] \
1377            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1378             print("Ignoring: Illegal value for thing type or position.")
1379         else:
1380             memthing = (type, posy, posx)
1381             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1382
1383
1384 def setter_map(maptype):
1385     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1386
1387     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1388     """
1389     @test_Thing_id
1390     def helper(str_int, mapline):
1391         val = integer_test(str_int, 0, 255)
1392         if None != val:
1393             if val >= world_db["MAP_LENGTH"]:
1394                 print("Illegal value for map line number.")
1395             elif len(mapline) != world_db["MAP_LENGTH"]:
1396                 print("Map line length is unequal map width.")
1397             else:
1398                 length = world_db["MAP_LENGTH"]
1399                 map = None
1400                 if not world_db["Things"][command_tid.id][maptype]:
1401                     map = bytearray(b' ' * (length ** 2))
1402                 else:
1403                     map = world_db["Things"][command_tid.id][maptype]
1404                 map[val * length:(val * length) + length] = mapline.encode()
1405                 world_db["Things"][command_tid.id][maptype] = map
1406     return helper
1407
1408
1409 def setter_tpos(axis):
1410     """Generate setter for T_POSX or  T_POSY of selected Thing.
1411
1412     If world is active, rebuilds animate things' fovmap, player's memory map.
1413     """
1414     @test_Thing_id
1415     def helper(str_int):
1416         val = integer_test(str_int, 0, 255)
1417         if None != val:
1418             if val < world_db["MAP_LENGTH"]:
1419                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1420                 if world_db["WORLD_ACTIVE"] \
1421                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1422                     build_fov_map(world_db["Things"][command_tid.id])
1423                     if 0 == command_tid.id:
1424                         update_map_memory(world_db["Things"][command_tid.id])
1425             else:
1426                 print("Ignoring: Position is outside of map.")
1427     return helper
1428
1429
1430 def command_ttid(id_string):
1431     """Set ID of ThingType to manipulate. ID unused? Create new one.
1432
1433     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1434     """
1435     id = id_setter(id_string, "ThingTypes", command_ttid)
1436     if None != id:
1437         world_db["ThingTypes"][id] = {
1438             "TT_NAME": "(none)",
1439             "TT_CONSUMABLE": 0,
1440             "TT_LIFEPOINTS": 0,
1441             "TT_PROLIFERATE": 0,
1442             "TT_START_NUMBER": 0,
1443             "TT_SYMBOL": "?",
1444             "TT_CORPSE_ID": id
1445         }
1446
1447
1448 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1449
1450
1451 @test_ThingType_id
1452 def command_ttname(name):
1453     """Set TT_NAME of selected ThingType."""
1454     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1455
1456
1457 @test_ThingType_id
1458 def command_ttsymbol(char):
1459     """Set TT_SYMBOL of selected ThingType. """
1460     if 1 == len(char):
1461         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1462     else:
1463         print("Ignoring: Argument must be single character.")
1464
1465
1466 @test_ThingType_id
1467 def command_ttcorpseid(str_int):
1468     """Set TT_CORPSE_ID of selected ThingType."""
1469     val = integer_test(str_int, 0, 255)
1470     if None != val:
1471         if val in world_db["ThingTypes"]:
1472             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1473         else:
1474             print("Ignoring: Corpse ID belongs to no known ThignType.")
1475
1476
1477 def command_taid(id_string):
1478     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1479
1480     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1481     """
1482     id = id_setter(id_string, "ThingActions", command_taid, True)
1483     if None != id:
1484         world_db["ThingActions"][id] = {
1485             "TA_EFFORT": 1,
1486             "TA_NAME": "wait"
1487         }
1488
1489
1490 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1491
1492
1493 @test_ThingAction_id
1494 def command_taname(name):
1495     """Set TA_NAME of selected ThingAction.
1496
1497     The name must match a valid thing action function. If after the name
1498     setting no ThingAction with name "wait" remains, call set_world_inactive().
1499     """
1500     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1501        or name == "pick_up":
1502         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1503         if 1 == world_db["WORLD_ACTIVE"]:
1504             wait_defined = False
1505             for id in world_db["ThingActions"]:
1506                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1507                     wait_defined = True
1508                     break
1509             if not wait_defined:
1510                 set_world_inactive()
1511     else:
1512         print("Ignoring: Invalid action name.")
1513     # In contrast to the original,naming won't map a function to a ThingAction.
1514
1515
1516 def command_ai():
1517     """Call ai() on player Thing, then turn_over()."""
1518     ai(world_db["Things"][0])
1519     turn_over()
1520
1521
1522 """Commands database.
1523
1524 Map command start tokens to ([0]) number of expected command arguments, ([1])
1525 the command's meta-ness (i.e. is it to be written to the record file, is it to
1526 be ignored in replay mode if read from server input file), and ([2]) a function
1527 to be called on it.
1528 """
1529 commands_db = {
1530     "QUIT": (0, True, command_quit),
1531     "PING": (0, True, command_ping),
1532     "THINGS_HERE": (2, True, command_thingshere),
1533     "MAKE_WORLD": (1, False, command_makeworld),
1534     "SEED_MAP": (1, False, command_seedmap),
1535     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1536     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1537     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1538     "MAP_LENGTH": (1, False, command_maplength),
1539     "WORLD_ACTIVE": (1, False, command_worldactive),
1540     "TA_ID": (1, False, command_taid),
1541     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1542     "TA_NAME": (1, False, command_taname),
1543     "TT_ID": (1, False, command_ttid),
1544     "TT_NAME": (1, False, command_ttname),
1545     "TT_SYMBOL": (1, False, command_ttsymbol),
1546     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1547     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1548                                        0, 65535)),
1549     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1550                                          0, 255)),
1551     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1552                                         0, 255)),
1553     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1554     "T_ID": (1, False, command_tid),
1555     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1556     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1557     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1558     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1559     "T_COMMAND": (1, False, command_tcommand),
1560     "T_TYPE": (1, False, command_ttype),
1561     "T_CARRIES": (1, False, command_tcarries),
1562     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1563     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1564     "T_MEMTHING": (3, False, command_tmemthing),
1565     "T_POSY": (1, False, setter_tpos("Y")),
1566     "T_POSX": (1, False, setter_tpos("X")),
1567     "wait": (0, False, play_commander("wait")),
1568     "move": (1, False, play_commander("move")),
1569     "pick_up": (0, False, play_commander("pick_up")),
1570     "drop": (1, False, play_commander("drop", True)),
1571     "use": (1, False, play_commander("use", True)),
1572     "ai": (0, False, command_ai)
1573 }
1574
1575
1576 """World state database. With sane default values. (Randomness is in rand.)"""
1577 world_db = {
1578     "TURN": 0,
1579     "MAP_LENGTH": 64,
1580     "SEED_MAP": 0,
1581     "PLAYER_TYPE": 0,
1582     "WORLD_ACTIVE": 0,
1583     "ThingActions": {},
1584     "ThingTypes": {},
1585     "Things": {}
1586 }
1587
1588 """Mapping of direction names to internal direction chars."""
1589 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1590                  "west": "s", "north-west": "w", "north-east": "e"}
1591
1592 """File IO database."""
1593 io_db = {
1594     "path_save": "save",
1595     "path_record": "record",
1596     "path_worldconf": "confserver/world",
1597     "path_server": "server/",
1598     "path_in": "server/in",
1599     "path_out": "server/out",
1600     "path_worldstate": "server/worldstate",
1601     "tmp_suffix": "_tmp",
1602     "kicked_by_rival": False,
1603     "worldstate_updateable": False
1604 }
1605
1606
1607 try:
1608     libpr = prep_library()
1609     rand = RandomnessIO()
1610     opts = parse_command_line_arguments()
1611     setup_server_io()
1612     if None != opts.replay:
1613         replay_game()
1614     else:
1615         play_game()
1616 except SystemExit as exit:
1617     print("ABORTING: " + exit.args[0])
1618 except:
1619     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1620     raise
1621 finally:
1622     cleanup_server_io()