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