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