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