home · contact · privacy
Update/improve tests for lack of compiled files to run redo for.
[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         string = str(world_db["TURN"]) + "\n" + \
328                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
329                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
330                  inventory + "%\n" + \
331                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
332                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
333                  str(world_db["MAP_LENGTH"]) + "\n"
334         length = world_db["MAP_LENGTH"]
335         fov = bytearray(b' ' * (length ** 2))
336         for pos in range(length ** 2):
337             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
338                 fov[pos] = world_db["MAP"][pos]
339         for i in range(3):
340             draw_visible_Things(fov, i)
341         string = write_map(string, fov)
342         mem = world_db["Things"][0]["T_MEMMAP"][:]
343         for i in range(2):
344             for mt in world_db["Things"][0]["T_MEMTHING"]:
345                 consumable = world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]
346                 if (i == 0 and not consumable) or (i == 1 and consumable):
347                     c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
348                     mem[(mt[1] * length) + mt[2]] = ord(c)
349         string = write_map(string, mem)
350         atomic_write(io_db["path_worldstate"], string, delete=False)
351         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
352         io_db["worldstate_updateable"] = False
353
354
355 def replay_game():
356     """Replay game from record file.
357
358     Use opts.replay as breakpoint turn to which to replay automatically before
359     switching to manual input by non-meta commands in server input file
360     triggering further reads of record file. Ensure opts.replay is at least 1.
361     Run try_worldstate_update() before each interactive obey()/read_command().
362     """
363     if opts.replay < 1:
364         opts.replay = 1
365     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
366           " (if so late a turn is to be found).")
367     if not os.access(io_db["path_record"], os.F_OK):
368         raise SystemExit("No record file found to replay.")
369     io_db["file_record"] = open(io_db["path_record"], "r")
370     io_db["file_record"].prefix = "record file line "
371     io_db["file_record"].line_n = 1
372     while world_db["TURN"] < opts.replay:
373         line = io_db["file_record"].readline()
374         if "" == line:
375             break
376         obey(line.rstrip(), io_db["file_record"].prefix
377              + str(io_db["file_record"].line_n))
378         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
379     while True:
380         try_worldstate_update()
381         obey(read_command(), "in file", replay=True)
382
383
384 def play_game():
385     """Play game by server input file commands. Before, load save file found.
386
387     If no save file is found, a new world is generated from the commands in the
388     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
389     command and all that follow via the server input file. Run
390     try_worldstate_update() before each interactive obey()/read_command().
391     """
392     if os.access(io_db["path_save"], os.F_OK):
393         obey_lines_in_file(io_db["path_save"], "save")
394     else:
395         if not os.access(io_db["path_worldconf"], os.F_OK):
396             msg = "No world config file from which to start a new world."
397             raise SystemExit(msg)
398         obey_lines_in_file(io_db["path_worldconf"], "world config ",
399                            do_record=True)
400         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
401     while True:
402         try_worldstate_update()
403         obey(read_command(), "in file", do_record=True)
404
405
406 def remake_map():
407     """(Re-)make island map.
408
409     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
410     start with one land cell in the middle, then go into cycle of repeatedly
411     selecting a random sea cell and transforming it into land if it is neighbor
412     to land. The cycle ends when a land cell is due to be created at the map's
413     border. Then put some trees on the map (TODO: more precise algorithm desc).
414     """
415     def is_neighbor(coordinates, type):
416         y = coordinates[0]
417         x = coordinates[1]
418         length = world_db["MAP_LENGTH"]
419         ind = y % 2
420         diag_west = x + (ind > 0)
421         diag_east = x + (ind < (length - 1))
422         pos = (y * length) + x
423         if (y > 0 and diag_east
424             and type == chr(world_db["MAP"][pos - length + ind])) \
425            or (x < (length - 1)
426                and type == chr(world_db["MAP"][pos + 1])) \
427            or (y < (length - 1) and diag_east
428                and type == chr(world_db["MAP"][pos + length + ind])) \
429            or (y > 0 and diag_west
430                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
431            or (x > 0
432                and type == chr(world_db["MAP"][pos - 1])) \
433            or (y < (length - 1) and diag_west
434                and type == chr(world_db["MAP"][pos + length - (not ind)])):
435             return True
436         return False
437     store_seed = rand.seed
438     rand.seed = world_db["SEED_MAP"]
439     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
440     length = world_db["MAP_LENGTH"]
441     add_half_width = (not (length % 2)) * int(length / 2)
442     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
443     while (1):
444         y = rand.next() % length
445         x = rand.next() % length
446         pos = (y * length) + x
447         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
448             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
449                 break
450             world_db["MAP"][pos] = ord(".")
451     n_trees = int((length ** 2) / 16)
452     i_trees = 0
453     while (i_trees <= n_trees):
454         single_allowed = rand.next() % 32
455         y = rand.next() % length
456         x = rand.next() % length
457         pos = (y * length) + x
458         if "." == chr(world_db["MAP"][pos]) \
459           and ((not single_allowed) or is_neighbor((y, x), "X")):
460             world_db["MAP"][pos] = ord("X")
461             i_trees += 1
462     rand.seed = store_seed
463     # This all-too-precise replica of the original C code misses iter_limit().
464
465
466 def update_map_memory(t, age_map=True):
467     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
468     if not t["T_MEMMAP"]:
469         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
470     if not t["T_MEMDEPTHMAP"]:
471         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
472     ord_v = ord("v")
473     ord_0 = ord("0")
474     ord_space = ord(" ")
475     for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
476                 if ord_v == t["fovmap"][pos]]:
477         t["T_MEMDEPTHMAP"][pos] = ord_0
478         if ord_space == t["T_MEMMAP"][pos]:
479             t["T_MEMMAP"][pos] = world_db["MAP"][pos]
480     if age_map:
481         maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
482         memdepthmap = maptype.from_buffer(t["T_MEMDEPTHMAP"])
483         fovmap = maptype.from_buffer(t["fovmap"])
484         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
485     for mt in [mt for mt in t["T_MEMTHING"]
486                if "v" == chr(t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
487                                          + mt[2]])]:
488          t["T_MEMTHING"].remove(mt)
489     for id in [id for id in world_db["Things"]
490                if not world_db["Things"][id]["carried"]]:
491         type = world_db["Things"][id]["T_TYPE"]
492         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
493             y = world_db["Things"][id]["T_POSY"]
494             x = world_db["Things"][id]["T_POSX"]
495             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
496                 t["T_MEMTHING"].append((type, y, x))
497
498
499 def set_world_inactive():
500     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
501     server_test()
502     if os.access(io_db["path_worldstate"], os.F_OK):
503         os.remove(io_db["path_worldstate"])
504     world_db["WORLD_ACTIVE"] = 0
505
506
507 def integer_test(val_string, min, max=None):
508     """Return val_string if possible integer >= min and <= max, else None."""
509     try:
510         val = int(val_string)
511         if val < min or (max != None and val > max):
512             raise ValueError
513         return val
514     except ValueError:
515         msg = "Ignoring: Please use integer >= " + str(min)
516         if max != None:
517             msg += " and <= " + str(max)
518         msg += "."
519         print(msg)
520         return None
521
522
523 def setter(category, key, min, max=None):
524     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
525     if category is None:
526         def f(val_string):
527             val = integer_test(val_string, min, max)
528             if None != val:
529                 world_db[key] = val
530     else:
531         if category == "Thing":
532             id_store = command_tid
533             decorator = test_Thing_id
534         elif category == "ThingType":
535             id_store = command_ttid
536             decorator = test_ThingType_id
537         elif category == "ThingAction":
538             id_store = command_taid
539             decorator = test_ThingAction_id
540
541         @decorator
542         def f(val_string):
543             val = integer_test(val_string, min, max)
544             if None != val:
545                 world_db[category + "s"][id_store.id][key] = val
546     return f
547
548
549 def build_fov_map(t):
550     """Build Thing's FOV map."""
551     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
552     maptype = ctypes.c_char * len(world_db["MAP"])
553     test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"],
554                                maptype.from_buffer(t["fovmap"]),
555                                maptype.from_buffer(world_db["MAP"]))
556     if test:
557         raise RuntimeError("Malloc error in build_fov_Map().")
558
559
560 def decrement_lifepoints(t):
561     """Decrement t's lifepoints by 1, and if to zero, corpse it.
562
563     If t is the player avatar, only blank its fovmap, so that the client may
564     still display memory data. On non-player things, erase fovmap and memory.
565     """
566     t["T_LIFEPOINTS"] -= 1
567     if 0 == t["T_LIFEPOINTS"]:
568         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
569         if world_db["Things"][0] == t:
570             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
571             strong_write(io_db["file_out"], "LOG You die.\n")
572         else:
573             t["fovmap"] = False
574             t["T_MEMMAP"] = False
575             t["T_MEMDEPTHMAP"] = False
576             t["T_MEMTHING"] = []
577             strong_write(io_db["file_out"], "LOG It dies.\n")
578
579
580 def mv_yx_in_dir_legal(dir, y, x):
581     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
582     dir_c = dir.encode("ascii")[0]
583     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
584     if -1 == test:
585         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
586     return (test, libpr.result_y(), libpr.result_x())
587
588
589 def actor_wait(t):
590     """Make t do nothing (but loudly, if player avatar)."""
591     if t == world_db["Things"][0]:
592         strong_write(io_db["file_out"], "LOG You wait.\n")
593
594
595 def actor_move(t):
596     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
597     passable = False
598     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
599                                      t["T_POSY"], t["T_POSX"])
600     if 1 == move_result[0]:
601         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
602         passable = "." == chr(world_db["MAP"][pos])
603         hitted = [id for id in world_db["Things"]
604                   if world_db["Things"][id] != t
605                   if world_db["Things"][id]["T_LIFEPOINTS"]
606                   if world_db["Things"][id]["T_POSY"] == move_result[1]
607                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
608         if len(hitted):
609             hit_id = hitted[0]
610             hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
611             hitter = "You" if t == world_db["Things"][0] else hitter_name
612             hitted_type = world_db["Things"][hit_id]["T_TYPE"]
613             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
614             hitted = "you" if hit_id == 0 else hitted_name
615             verb = " wound " if hitter == "You" else " wounds "
616             strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted +
617                                             ".\n")
618             decrement_lifepoints(world_db["Things"][hit_id])
619             return
620     dir = [dir for dir in directions_db
621            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
622     if passable:
623         t["T_POSY"] = move_result[1]
624         t["T_POSX"] = move_result[2]
625         for id in t["T_CARRIES"]:
626             world_db["Things"][id]["T_POSY"] = move_result[1]
627             world_db["Things"][id]["T_POSX"] = move_result[2]
628         build_fov_map(t)
629         if t == world_db["Things"][0]:
630             strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
631     elif t == world_db["Things"][0]:
632         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
633
634
635 def actor_pick_up(t):
636     """Make t pick up (topmost?) Thing from ground into inventory."""
637     # Topmostness is actually not defined so far. Picks Thing with highest ID.
638     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
639            if not world_db["Things"][id]["carried"]
640            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
641            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
642     if len(ids):
643         highest_id = 0
644         for id in ids:
645             if id > highest_id:
646                 highest_id = id
647         world_db["Things"][highest_id]["carried"] = True
648         t["T_CARRIES"].append(highest_id)
649         if t == world_db["Things"][0]:
650             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
651     elif t == world_db["Things"][0]:
652         err = "You try to pick up an object, but there is none."
653         strong_write(io_db["file_out"], "LOG " + err + "\n")
654
655
656 def actor_drop(t):
657     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
658     # TODO: Handle case where T_ARGUMENT matches nothing.
659     if len(t["T_CARRIES"]):
660         id = t["T_CARRIES"][t["T_ARGUMENT"]]
661         t["T_CARRIES"].remove(id)
662         world_db["Things"][id]["carried"] = False
663         if t == world_db["Things"][0]:
664             strong_write(io_db["file_out"], "LOG You drop an object.\n")
665     elif t == world_db["Things"][0]:
666         err = "You try to drop an object, but you own none."
667         strong_write(io_db["file_out"], "LOG " + err + "\n")
668
669
670 def actor_use(t):
671     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
672     # TODO: Handle case where T_ARGUMENT matches nothing.
673     if len(t["T_CARRIES"]):
674         id = t["T_CARRIES"][t["T_ARGUMENT"]]
675         type = world_db["Things"][id]["T_TYPE"]
676         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
677             t["T_CARRIES"].remove(id)
678             del world_db["Things"][id]
679             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
680             if t == world_db["Things"][0]:
681                 strong_write(io_db["file_out"],
682                              "LOG You consume this object.\n")
683         elif t == world_db["Things"][0]:
684             strong_write(io_db["file_out"],
685                          "LOG You try to use this object, but fail.\n")
686     elif t == world_db["Things"][0]:
687         strong_write(io_db["file_out"],
688                      "LOG You try to use an object, but you own none.\n")
689
690
691 def thingproliferation(t):
692     """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
693
694     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
695     passable and not be inhabited by a Thing of the same type, or, if Thing is
696     animate, any other animate Thing. If there are several map cell candidates,
697     one is selected randomly.
698     """
699     def test_cell(t, y, x):
700         if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
701             for id in [id for id in world_db["Things"]
702                        if y == world_db["Things"][id]["T_POSY"]
703                        if x == world_db["Things"][id]["T_POSX"]
704                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
705                        or (t["T_LIFEPOINTS"] and
706                            world_db["Things"][id]["T_LIFEPOINTS"])]:
707                 return False
708             return True
709         return False
710     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
711     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
712         candidates = []
713         for dir in [directions_db[key] for key in directions_db]:
714             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
715             if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
716                 candidates.append((mv_result[1], mv_result[2]))
717         if len(candidates):
718             i = rand.next() % len(candidates)
719             id = id_setter(-1, "Things")
720             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
721             world_db["Things"][id] = newT
722
723
724 def try_healing(t):
725     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
726
727     On success, decrease satiation score by 32.
728     """
729     if t["T_SATIATION"] > 0 \
730        and t["T_LIFEPOINTS"] < \
731            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
732        and 0 == (rand.next() % 31) \
733        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
734                               if world_db["ThingActions"][id]["TA_NAME"] ==
735                                  "wait"][0]:
736         t["T_LIFEPOINTS"] += 1
737         t["T_SATIATION"] -= 32
738         if t == world_db["Things"][0]:
739             strong_write(io_db["file_out"], "LOG You heal.\n")
740         else:
741             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
742             strong_write(io_db["file_out"], "LOG " + name + "heals.\n")
743
744
745 def hunger(t):
746     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
747     if t["T_SATIATION"] > -32768:
748         t["T_SATIATION"] -= 1
749     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
750     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
751         raise RuntimeError("A thing that should not hunger is hungering.")
752     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
753     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
754         if t == world_db["Things"][0]:
755             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
756         else:
757             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
758             strong_write(io_db["file_out"], "LOG " + name +
759                                             " suffers from hunger.\n")
760         decrement_lifepoints(t)
761
762
763 def get_dir_to_target(t, filter):
764     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
765
766     The path-wise nearest target is chosen, via the shortest available path.
767     Target must not be t. On succcess, return positive value, else False.
768     Filters:
769     "a": Thing in FOV is below a certain distance, animate, but of ThingType
770          that is not t's, and starts out weaker than t is; build path as
771          avoiding things of t's ThingType
772     "f": neighbor cell (not inhabited by any animate Thing) further away from
773          animate Thing not further than x steps away and in FOV and of a
774          ThingType that is not t's, and starts out stronger or as strong as t
775          is currently; or (cornered), if no such flight cell, but Thing of
776          above criteria is too near,1 a cell closer to it, or, if less near,
777          just wait
778     "c": Thing in memorized map is consumable
779     "s": memory map cell with greatest-reachable degree of unexploredness
780     """
781
782     def zero_score_map_where_char_on_memdepthmap(c):
783         maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
784         map = maptype.from_buffer(t["T_MEMDEPTHMAP"])
785         test = libpr.zero_score_map_where_char_on_memdepthmap(c, map)
786         if test:
787             raise RuntimeError("No score map allocated for "
788                                "zero_score_map_where_char_on_memdepthmap().")
789
790     def set_map_score(pos, score):
791         test = libpr.set_map_score(pos, score)
792         if test:
793             raise RuntimeError("No score map allocated for set_map_score().")
794
795     def get_map_score(pos):
796         result = libpr.get_map_score(pos)
797         if result < 0:
798             raise RuntimeError("No score map allocated for get_map_score().")
799         return result
800
801     def seeing_thing():
802         if t["fovmap"] and ("a" == filter or "f" == filter):
803             for id in world_db["Things"]:
804                 Thing = world_db["Things"][id]
805                 if Thing != t and Thing["T_LIFEPOINTS"] and \
806                    t["T_TYPE"] != Thing["T_TYPE"] and \
807                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
808                                           * world_db["MAP_LENGTH"])
809                                           + Thing["T_POSX"]]):
810                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
811                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
812                                           t["T_LIFEPOINTS"]) \
813                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
814                                              t["T_LIFEPOINTS"]):
815                         return True
816         elif t["T_MEMMAP"] and "c" == filter:
817             for mt in t["T_MEMTHING"]:
818                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
819                                          + mt[2]]) \
820                    and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
821                     return True
822         return False
823
824     def init_score_map():
825         test = libpr.init_score_map()
826         if test:
827             raise RuntimeError("Malloc error in init_score_map().")
828         ord_dot = ord(".")
829         ord_v = ord("v")
830         ord_blank = ord(" ")
831         for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
832                   if ord_dot == t["T_MEMMAP"][i]]:
833             set_map_score(i, 65535 - 1)
834         if "a" == filter:
835             for id in world_db["Things"]:
836                 Thing = world_db["Things"][id]
837                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
838                       + Thing["T_POSX"]
839                 if t != Thing and Thing["T_LIFEPOINTS"] and \
840                    t["T_TYPE"] != Thing["T_TYPE"] and \
841                    ord_v == t["fovmap"][pos] and \
842                    t["T_LIFEPOINTS"] > \
843                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
844                     set_map_score(pos, 0)
845                 elif t["T_TYPE"] == Thing["T_TYPE"]:
846                     set_map_score(pos, 65535)
847         elif "f" == filter:
848             for id in [id for id in world_db["Things"]
849                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
850                 Thing = world_db["Things"][id]
851                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
852                       + Thing["T_POSX"]
853                 if t["T_TYPE"] != Thing["T_TYPE"] and \
854                    ord_v == t["fovmap"][pos] and \
855                    t["T_LIFEPOINTS"] <= \
856                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
857                     set_map_score(pos, 0)
858         elif "c" == filter:
859             for mt in [mt for mt in t["T_MEMTHING"]
860                        if ord_blank != t["T_MEMMAP"][mt[1]
861                                                     * world_db["MAP_LENGTH"]
862                                                     + mt[2]]
863                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
864                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
865         elif "s" == filter:
866             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
867
868     def rand_target_dir(neighbors, cmp, dirs):
869         candidates = []
870         n_candidates = 0
871         for i in range(len(dirs)):
872             if cmp == neighbors[i]:
873                 candidates.append(dirs[i])
874                 n_candidates += 1
875         return candidates[rand.next() % n_candidates] if n_candidates else 0
876
877     def get_neighbor_scores(dirs, eye_pos):
878         scores = []
879         if libpr.ready_neighbor_scores(eye_pos):
880             raise RuntimeError("No score map allocated for " +
881                                "ready_neighbor_scores.()")
882         for i in range(len(dirs)):
883             scores.append(libpr.get_neighbor_score(i))
884         return scores
885
886     def get_dir_from_neighbors():
887         dir_to_target = False
888         dirs = "edcxsw"
889         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
890         neighbors = get_neighbor_scores(dirs, eye_pos)
891         if "f" == filter:
892             inhabited = [world_db["Things"][id]["T_POSY"]
893                          * world_db["MAP_LENGTH"]
894                          + world_db["Things"][id]["T_POSX"]
895                          for id in world_db["Things"]
896                          if world_db["Things"][id]["T_LIFEPOINTS"]]
897             for i in range(len(dirs)):
898                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
899                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
900                           + libpr.result_x()
901                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
902                     neighbors[i] = 65535
903                     break
904         minmax_start = 0 if "f" == filter else 65535 - 1
905         minmax_neighbor = minmax_start
906         for i in range(len(dirs)):
907             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
908                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
909                or ("f" != filter and minmax_neighbor > neighbors[i]):
910                 minmax_neighbor = neighbors[i]
911         if minmax_neighbor != minmax_start:
912             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
913         if "f" == filter:
914             if not dir_to_target:
915                 if 1 == get_map_score(eye_pos):
916                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
917                 elif 3 >= get_map_score(eye_pos):
918                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
919                                       if
920                                       world_db["ThingActions"][id]["TA_NAME"]
921                                          == "wait"][0]
922                     return 1
923             elif dir_to_target and 3 < get_map_score(eye_pos):
924                 dir_to_target = 0
925         elif "a" == filter and 10 <= get_map_score(eye_pos):
926             dir_to_target = 0
927         return dir_to_target
928
929     dir_to_target = False
930     mem_depth_c = b' '
931     run_i = 9 + 1 if "s" == filter else 1
932     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
933         run_i -= 1
934         init_score_map()
935         mem_depth_c = b'9' if b' ' == mem_depth_c \
936                            else bytes([mem_depth_c[0] - 1])
937         if libpr.dijkstra_map():
938             raise RuntimeError("No score map allocated for dijkstra_map().")
939         dir_to_target = get_dir_from_neighbors()
940         libpr.free_score_map()
941         if dir_to_target and str == type(dir_to_target):
942             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
943                               if world_db["ThingActions"][id]["TA_NAME"]
944                                  == "move"][0]
945             t["T_ARGUMENT"] = ord(dir_to_target)
946     return dir_to_target
947
948
949 def standing_on_consumable(t):
950     """Return True/False whether t is standing on a consumable."""
951     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
952                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
953                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
954                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
955                           ["TT_CONSUMABLE"]]:
956         return True
957     return False
958
959
960 def get_inventory_slot_to_consume(t):
961     """Return slot Id of strongest consumable in t's inventory, else -1."""
962     cmp_consumability = 0
963     selection = -1
964     i = 0
965     for id in t["T_CARRIES"]:
966         type = world_db["Things"][id]["T_TYPE"]
967         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
968             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
969             selection = i
970         i += 1
971     return selection
972
973
974 def ai(t):
975     """Determine next command/argment for actor t via AI algorithms.
976
977     AI will look for, and move towards, enemies (animate Things not of their
978     own ThingType); if they see none, they will consume consumables in their
979     inventory; if there are none, they will pick up what they stand on if they
980     stand on consumables; if they stand on none, they will move towards the
981     next consumable they see or remember on the map; if they see or remember
982     none, they will explore parts of the map unseen since ever or for at least
983     one turn; if there is nothing to explore, they will simply wait.
984     """
985     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
986                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
987     if not get_dir_to_target(t, "f"):
988         sel = get_inventory_slot_to_consume(t)
989         if -1 != sel:
990             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
991                               if world_db["ThingActions"][id]["TA_NAME"]
992                                  == "use"][0]
993             t["T_ARGUMENT"] = sel
994         elif standing_on_consumable(t):
995             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
996                               if world_db["ThingActions"][id]["TA_NAME"]
997                                  == "pick_up"][0]
998         elif (not get_dir_to_target(t, "c")) and \
999              (not get_dir_to_target(t, "a")):
1000             get_dir_to_target(t, "s")
1001
1002
1003 def turn_over():
1004     """Run game world and its inhabitants until new player input expected."""
1005     id = 0
1006     whilebreaker = False
1007     while world_db["Things"][0]["T_LIFEPOINTS"]:
1008         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1009             if not id in world_db["Things"] or \
1010                world_db["Things"][id]["carried"]:   # May have been consumed or
1011                 continue                            # picked up during turn …
1012             Thing = world_db["Things"][id]
1013             if Thing["T_LIFEPOINTS"]:
1014                 if not Thing["T_COMMAND"]:
1015                     update_map_memory(Thing)
1016                     if 0 == id:
1017                         whilebreaker = True
1018                         break
1019                     ai(Thing)
1020                 try_healing(Thing)
1021                 Thing["T_PROGRESS"] += 1
1022                 taid = [a for a in world_db["ThingActions"]
1023                           if a == Thing["T_COMMAND"]][0]
1024                 ThingAction = world_db["ThingActions"][taid]
1025                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1026                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
1027                     Thing["T_COMMAND"] = 0
1028                     Thing["T_PROGRESS"] = 0
1029                 hunger(Thing)
1030             thingproliferation(Thing)
1031         if whilebreaker:
1032             break
1033         world_db["TURN"] += 1
1034
1035
1036 def new_Thing(type, pos=(0, 0)):
1037     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1038     thing = {
1039         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1040         "T_ARGUMENT": 0,
1041         "T_PROGRESS": 0,
1042         "T_SATIATION": 0,
1043         "T_COMMAND": 0,
1044         "T_TYPE": type,
1045         "T_POSY": pos[0],
1046         "T_POSX": pos[1],
1047         "T_CARRIES": [],
1048         "carried": False,
1049         "T_MEMTHING": [],
1050         "T_MEMMAP": False,
1051         "T_MEMDEPTHMAP": False,
1052         "fovmap": False
1053     }
1054     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1055         build_fov_map(thing)
1056     return thing
1057
1058
1059 def id_setter(id, category, id_store=False, start_at_1=False):
1060     """Set ID of object of category to manipulate ID unused? Create new one.
1061
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, otherwise the new object's
1066     ID.
1067     """
1068     min = 0 if start_at_1 else -1
1069     if str == type(id):
1070         id = integer_test(id, min)
1071     if None != id:
1072         if id in world_db[category]:
1073             if id_store:
1074                 id_store.id = id
1075             return None
1076         else:
1077             if (start_at_1 and 0 == id) \
1078                or ((not start_at_1) and (id < 0)):
1079                 id = 0 if start_at_1 else -1
1080                 while 1:
1081                     id = id + 1
1082                     if id not in world_db[category]:
1083                         break
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()