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