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