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