home · contact · privacy
Abolish upper ID limits.
[plomrogue] / plomrogue-server.py
index daff24f17310169c77de8d7c1a8767b3f95bb16d..772277c8b3916de15234d838004589a9dff210de 100755 (executable)
@@ -1,3 +1,10 @@
+#!/usr/bin/python3
+
+# This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
+# or any later version. For details on its copyright, license, and warranties,
+# see the file NOTICE in the root directory of the PlomRogue source package.
+
+
 import argparse
 import errno
 import os
@@ -26,30 +33,17 @@ def prep_library():
     """Prepare ctypes library at ./libplomrogue.so"""
     libpath = ("./libplomrogue.so")
     if not os.access(libpath, os.F_OK):
-        raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
+        raise SystemExit("No library " + libpath +
+                         ", run ./compile-server.sh first?")
     libpr = ctypes.cdll.LoadLibrary(libpath)
-    libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
     libpr.seed_rrand.restype = ctypes.c_uint32
-    libpr.rrand.argtypes = []
-    libpr.rrand.restype = ctypes.c_uint16
-    libpr.set_maplength.argtypes = [ctypes.c_uint16]
-    libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8,
-                                              ctypes.c_uint8]
-    libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8
-    libpr.result_y.restype = ctypes.c_uint8
-    libpr.result_x.restype = ctypes.c_uint8
-    libpr.set_maplength(world_db["MAP_LENGTH"])
-    libpr.build_fov_map.argtypes = [ctypes.c_uint8, ctypes.c_uint8,
-                                    ctypes.c_char_p, ctypes.c_char_p]
-    libpr.build_fov_map.restype = ctypes.c_uint8
     return libpr
 
 
 def strong_write(file, string):
-    """Apply write(string), flush(), and os.fsync() to file."""
+    """Apply write(string), then flush()."""
     file.write(string)
     file.flush()
-    os.fsync(file)
 
 
 def setup_server_io():
@@ -69,6 +63,9 @@ def setup_server_io():
         if os.access(path_tmp, os.F_OK):
             raise SystemExit(msg)
     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
+    io_db["save_wait"] = 0
+    io_db["verbose"] = False
+    io_db["record_chunk"] = ""
     os.makedirs(io_db["path_server"], exist_ok=True)
     io_db["file_out"] = open(io_db["path_out"], "w")
     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
@@ -86,11 +83,11 @@ def cleanup_server_io():
     def helper(file_key, path_key):
         if file_key in io_db:
             io_db[file_key].close()
-            if not io_db["kicked_by_rival"] \
-               and os.access(io_db[path_key], os.F_OK):
-                os.remove(io_db[path_key])
-    helper("file_out", "path_out")
+        if not io_db["kicked_by_rival"] \
+           and os.access(io_db[path_key], os.F_OK):
+            os.remove(io_db[path_key])
     helper("file_in", "path_in")
+    helper("file_out", "path_out")
     helper("file_worldstate", "path_worldstate")
     if "file_record" in io_db:
         io_db["file_record"].close()
@@ -103,12 +100,15 @@ def obey(command, prefix, replay=False, do_record=False):
     a non-meta command from the commands_db merely triggers obey() on the next
     command from the records file. If not, non-meta commands set
     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
-    do_record is set, are recorded via record(), and save_world() is called.
-    The prefix string is inserted into the server's input message between its
-    beginning 'input ' & ':'. All activity is preceded by a server_test() call.
+    do_record is set, are recorded to io_db["record_chunk"], and save_world()
+    is called (and io_db["record_chunk"] written) if 15 seconds have passed
+    since the last time it was called. The prefix string is inserted into the
+    server's input message between its beginning 'input ' and ':'. All activity
+    is preceded by a server_test() call.
     """
     server_test()
-    print("input " + prefix + ": " + command)
+    if io_db["verbose"]:
+        print("input " + prefix + ": " + command)
     try:
         tokens = shlex.split(command, comments=True)
     except ValueError as err:
@@ -130,8 +130,13 @@ def obey(command, prefix, replay=False, do_record=False):
         else:
             commands_db[tokens[0]][2](*tokens[1:])
             if do_record:
-                record(command)
-                save_world()
+                io_db["record_chunk"] += command + "\n"
+                if time.time() > io_db["save_wait"] + 15:
+                    atomic_write(io_db["path_record"], io_db["record_chunk"],
+                                 do_append=True)
+                    save_world()
+                    io_db["record_chunk"] = ""
+                    io_db["save_wait"] = time.time()
             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
     elif 0 != len(tokens):
         print("Invalid command/argument, or bad number of tokens.")
@@ -153,17 +158,8 @@ def atomic_write(path, text, do_append=False):
     os.rename(path_tmp, path)
 
 
-def record(command):
-    """Append command string plus newline to record file. (Atomic.)"""
-    # This misses some optimizations from the original record(), namely only
-    # finishing the atomic write with expensive flush() and fsync() every 15
-    # seconds unless explicitely forced. Implement as needed.
-    atomic_write(io_db["path_record"], command + "\n", do_append=True)
-
-
 def save_world():
     """Save all commands needed to reconstruct current world state."""
-    # TODO: Misses same optimizations as record() from the original record().
 
     def quote(string):
         string = string.replace("\u005C", '\u005C\u005C')
@@ -204,8 +200,10 @@ def save_world():
 
     string = ""
     for key in world_db:
-        if dict != type(world_db[key]) and key != "MAP":
+        if dict != type(world_db[key]) and key != "MAP" and \
+           key != "WORLD_ACTIVE" and key != "SEED_MAP":
             string = string + key + " " + str(world_db[key]) + "\n"
+    string = string + "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n"
     string = string + helper("ThingActions", "TA_ID")
     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
     for id in world_db["ThingTypes"]:
@@ -242,6 +240,9 @@ def parse_command_line_arguments():
     parser = argparse.ArgumentParser()
     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
                         action='store')
+    parser.add_argument('-l', nargs="?", const="save", dest='savefile',
+                        action="store")
+    parser.add_argument('-v', dest='verbose', action='store_true')
     opts, unknown = parser.parse_known_args()
     return opts
 
@@ -435,6 +436,7 @@ def remake_map():
             return True
         return False
     store_seed = rand.seed
+    rand.seed = world_db["SEED_MAP"]
     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
     length = world_db["MAP_LENGTH"]
     add_half_width = (not (length % 2)) * int(length / 2)
@@ -462,27 +464,32 @@ def remake_map():
     # This all-too-precise replica of the original C code misses iter_limit().
 
 
-def update_map_memory(t):
+def update_map_memory(t, age_map=True):
     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
     if not t["T_MEMMAP"]:
         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
     if not t["T_MEMDEPTHMAP"]:
         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
-    for pos in range(world_db["MAP_LENGTH"] ** 2):
-        if "v" == chr(t["fovmap"][pos]):
-            t["T_MEMDEPTHMAP"][pos] = ord("0")
-            if " " == chr(t["T_MEMMAP"][pos]):
-                t["T_MEMMAP"][pos] = world_db["MAP"][pos]
-            continue
-        if ord('0') <= t["T_MEMDEPTHMAP"][pos] \
-           and ord('9') >= t["T_MEMDEPTHMAP"][pos] \
-           and not rand.next() % (2 ** (t["T_MEMDEPTHMAP"][pos] - 48)):
-            t["T_MEMDEPTHMAP"][pos] += 1
+    ord_v = ord("v")
+    ord_0 = ord("0")
+    ord_9 = ord("9")
+    ord_space = ord(" ")
+    for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
+                if ord_v == t["fovmap"][pos]]:
+        t["T_MEMDEPTHMAP"][pos] = ord_0
+        if ord_space == t["T_MEMMAP"][pos]:
+            t["T_MEMMAP"][pos] = world_db["MAP"][pos]
+    if age_map:
+        maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
+        memdepthmap = maptype.from_buffer(t["T_MEMDEPTHMAP"])
+        fovmap = maptype.from_buffer(t["fovmap"])
+        libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
     for mt in [mt for mt in t["T_MEMTHING"]
                if "v" == chr(t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
                                          + mt[2]])]:
-            t["T_MEMTHING"].remove(mt)
-    for id in world_db["Things"]:
+         t["T_MEMTHING"].remove(mt)
+    for id in [id for id in world_db["Things"]
+               if not world_db["Things"][id]["carried"]]:
         type = world_db["Things"][id]["T_TYPE"]
         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
             y = world_db["Things"][id]["T_POSY"]
@@ -499,20 +506,23 @@ def set_world_inactive():
     world_db["WORLD_ACTIVE"] = 0
 
 
-def integer_test(val_string, min, max):
-    """Return val_string if possible integer >= min and <= max, else None."""
+def integer_test(val_string, min, max=None):
+    """Return val_string if integer >= min & (if max set) <= max, else None."""
     try:
         val = int(val_string)
-        if val < min or val > max:
+        if val < min or (max != None and val > max):
             raise ValueError
         return val
     except ValueError:
-        print("Ignoring: Please use integer >= " + str(min) + " and <= " +
-              str(max) + ".")
+        msg = "Ignoring: Please use integer >= " + str(min)
+        if max != None:
+            msg += " and <= " + str(max)
+        msg += "."
+        print(msg)
         return None
 
 
-def setter(category, key, min, max):
+def setter(category, key, min, max=None):
     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
     if category is None:
         def f(val_string):
@@ -587,7 +597,8 @@ def actor_wait(t):
 def actor_move(t):
     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
     passable = False
-    move_result = mv_yx_in_dir_legal(t["T_ARGUMENT"], t["T_POSY"], t["T_POSX"])
+    move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
+                                     t["T_POSY"], t["T_POSX"])
     if 1 == move_result[0]:
         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
         passable = "." == chr(world_db["MAP"][pos])
@@ -609,7 +620,7 @@ def actor_move(t):
             decrement_lifepoints(world_db["Things"][hit_id])
             return
     dir = [dir for dir in directions_db
-           if directions_db[dir] == t["T_ARGUMENT"]][0]
+           if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
     if passable:
         t["T_POSY"] = move_result[1]
         t["T_POSX"] = move_result[2]
@@ -617,21 +628,26 @@ def actor_move(t):
             world_db["Things"][id]["T_POSY"] = move_result[1]
             world_db["Things"][id]["T_POSX"] = move_result[2]
         build_fov_map(t)
-        strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
-    else:
+        if t == world_db["Things"][0]:
+            strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
+    elif t == world_db["Things"][0]:
         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
 
 
 def actor_pick_up(t):
     """Make t pick up (topmost?) Thing from ground into inventory."""
-    # Topmostness is actually not defined so far.
+    # Topmostness is actually not defined so far. Picks Thing with highest ID.
     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
            if not world_db["Things"][id]["carried"]
            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
     if len(ids):
-        world_db["Things"][ids[0]]["carried"] = True
-        t["T_CARRIES"].append(ids[0])
+        highest_id = 0
+        for id in ids:
+            if id > highest_id:
+                highest_id = id
+        world_db["Things"][highest_id]["carried"] = True
+        t["T_CARRIES"].append(highest_id)
         if t == world_db["Things"][0]:
             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
     elif t == world_db["Things"][0]:
@@ -663,15 +679,15 @@ def actor_use(t):
             t["T_CARRIES"].remove(id)
             del world_db["Things"][id]
             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
-            t["T_LIFEPOINTS"] += 1
-            # Wrongly increment HPs is a replica of the original code.
-            strong_write(io_db["file_out"], "LOG You consume this object.\n")
-        else:
-            strong_write(io_db["file_out"], "LOG You try to use this object," +
-                                            "but fail.\n")
-    else:
-        strong_write(io_db["file_out"], "LOG You try to use an object, but " +
-                                        "you own none.\n")
+            if t == world_db["Things"][0]:
+                strong_write(io_db["file_out"],
+                             "LOG You consume this object.\n")
+        elif t == world_db["Things"][0]:
+            strong_write(io_db["file_out"],
+                         "LOG You try to use this object, but fail.\n")
+    elif t == world_db["Things"][0]:
+        strong_write(io_db["file_out"],
+                     "LOG You try to use an object, but you own none.\n")
 
 
 def thingproliferation(t):
@@ -746,12 +762,255 @@ def hunger(t):
         decrement_lifepoints(t)
 
 
+def get_dir_to_target(t, filter):
+    """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
+
+    The path-wise nearest target is chosen, via the shortest available path.
+    Target must not be t. On succcess, return positive value, else False.
+    Filters:
+    "a": Thing in FOV is below a certain distance, animate, but of ThingType
+         that is not t's, and starts out weaker than t is; build path as
+         avoiding things of t's ThingType
+    "f": neighbor cell (not inhabited by any animate Thing) further away from
+         animate Thing not further than x steps away and in FOV and of a
+         ThingType that is not t's, and starts out stronger or as strong as t
+         is currently; or (cornered), if no such flight cell, but Thing of
+         above criteria is too near,1 a cell closer to it, or, if less near,
+         just wait
+    "c": Thing in memorized map is consumable
+    "s": memory map cell with greatest-reachable degree of unexploredness
+    """
+
+    def zero_score_map_where_char_on_memdepthmap(c):
+        maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
+        map = maptype.from_buffer(t["T_MEMDEPTHMAP"])
+        test = libpr.zero_score_map_where_char_on_memdepthmap(c, map)
+        if test:
+            raise RuntimeError("No score map allocated for "
+                               "zero_score_map_where_char_on_memdepthmap().")
+
+    def set_map_score(pos, score):
+        test = libpr.set_map_score(pos, score)
+        if test:
+            raise RuntimeError("No score map allocated for set_map_score().")
+
+    def get_map_score(pos):
+        result = libpr.get_map_score(pos)
+        if result < 0:
+            raise RuntimeError("No score map allocated for get_map_score().")
+        return result
+
+    def seeing_thing():
+        if t["fovmap"] and ("a" == filter or "f" == filter):
+            for id in world_db["Things"]:
+                Thing = world_db["Things"][id]
+                if Thing != t and Thing["T_LIFEPOINTS"] and \
+                   t["T_TYPE"] != Thing["T_TYPE"] and \
+                   'v' == chr(t["fovmap"][(Thing["T_POSY"]
+                                          * world_db["MAP_LENGTH"])
+                                          + Thing["T_POSX"]]):
+                    ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
+                    if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
+                                          t["T_LIFEPOINTS"]) \
+                       or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
+                                             t["T_LIFEPOINTS"]):
+                        return True
+        elif t["T_MEMMAP"] and "c" == filter:
+            for mt in t["T_MEMTHING"]:
+                if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
+                                         + mt[2]]) \
+                   and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
+                    return True
+        return False
+
+    def init_score_map():
+        test = libpr.init_score_map()
+        if test:
+            raise RuntimeError("Malloc error in init_score_map().")
+        ord_dot = ord(".")
+        ord_v = ord("v")
+        ord_blank = ord(" ")
+        for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
+                  if ord_dot == t["T_MEMMAP"][i]]:
+            set_map_score(i, 65535 - 1)
+        if "a" == filter:
+            for id in world_db["Things"]:
+                Thing = world_db["Things"][id]
+                pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
+                      + Thing["T_POSX"]
+                if t != Thing and Thing["T_LIFEPOINTS"] and \
+                   t["T_TYPE"] != Thing["T_TYPE"] and \
+                   ord_v == t["fovmap"][pos] and \
+                   t["T_LIFEPOINTS"] > \
+                   world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
+                    set_map_score(pos, 0)
+                elif t["T_TYPE"] == Thing["T_TYPE"]:
+                    set_map_score(pos, 65535)
+        elif "f" == filter:
+            for id in [id for id in world_db["Things"]
+                       if world_db["Things"][id]["T_LIFEPOINTS"]]:
+                Thing = world_db["Things"][id]
+                pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
+                      + Thing["T_POSX"]
+                if t["T_TYPE"] != Thing["T_TYPE"] and \
+                   ord_v == t["fovmap"][pos] and \
+                   t["T_LIFEPOINTS"] <= \
+                   world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
+                    set_map_score(pos, 0)
+        elif "c" == filter:
+            for mt in [mt for mt in t["T_MEMTHING"]
+                       if ord_blank != t["T_MEMMAP"][mt[1]
+                                                    * world_db["MAP_LENGTH"]
+                                                    + mt[2]]
+                       if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
+                set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
+        elif "s" == filter:
+            zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
+
+    def rand_target_dir(neighbors, cmp, dirs):
+        candidates = []
+        n_candidates = 0
+        for i in range(len(dirs)):
+            if cmp == neighbors[i]:
+                candidates.append(dirs[i])
+                n_candidates += 1
+        return candidates[rand.next() % n_candidates] if n_candidates else 0
+
+    def get_neighbor_scores(dirs, eye_pos):
+        scores = []
+        if libpr.ready_neighbor_scores(eye_pos):
+            raise RuntimeError("No score map allocated for " +
+                               "ready_neighbor_scores.()")
+        for i in range(len(dirs)):
+            scores.append(libpr.get_neighbor_score(i))
+        return scores
+
+    def get_dir_from_neighbors():
+        dir_to_target = False
+        dirs = "edcxsw"
+        eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
+        neighbors = get_neighbor_scores(dirs, eye_pos)
+        if "f" == filter:
+            inhabited = [world_db["Things"][id]["T_POSY"]
+                         * world_db["MAP_LENGTH"]
+                         + world_db["Things"][id]["T_POSX"]
+                         for id in world_db["Things"]
+                         if world_db["Things"][id]["T_LIFEPOINTS"]]
+            for i in range(len(dirs)):
+                mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
+                pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
+                          + libpr.result_x()
+                for pos in [pos for pos in inhabited if pos == pos_cmp]:
+                    neighbors[i] = 65535
+                    break
+        minmax_start = 0 if "f" == filter else 65535 - 1
+        minmax_neighbor = minmax_start
+        for i in range(len(dirs)):
+            if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
+                minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
+               or ("f" != filter and minmax_neighbor > neighbors[i]):
+                minmax_neighbor = neighbors[i]
+        if minmax_neighbor != minmax_start:
+            dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
+        if "f" == filter:
+            if not dir_to_target:
+                if 1 == get_map_score(eye_pos):
+                    dir_to_target = rand_target_dir(neighbors, 0, dirs)
+                elif 3 >= get_map_score(eye_pos):
+                    t["T_COMMAND"] = [id for id in world_db["ThingActions"]
+                                      if
+                                      world_db["ThingActions"][id]["TA_NAME"]
+                                         == "wait"][0]
+                    return 1
+            elif dir_to_target and 3 < get_map_score(eye_pos):
+                dir_to_target = 0
+        elif "a" == filter and 10 <= get_map_score(eye_pos):
+            dir_to_target = 0
+        return dir_to_target
+
+    dir_to_target = False
+    mem_depth_c = b' '
+    run_i = 9 + 1 if "s" == filter else 1
+    while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
+        run_i -= 1
+        init_score_map()
+        mem_depth_c = b'9' if b' ' == mem_depth_c \
+                           else bytes([mem_depth_c[0] - 1])
+        if libpr.dijkstra_map():
+            raise RuntimeError("No score map allocated for dijkstra_map().")
+        dir_to_target = get_dir_from_neighbors()
+        libpr.free_score_map()
+        if dir_to_target and str == type(dir_to_target):
+            t["T_COMMAND"] = [id for id in world_db["ThingActions"]
+                              if world_db["ThingActions"][id]["TA_NAME"]
+                                 == "move"][0]
+            t["T_ARGUMENT"] = ord(dir_to_target)
+    return dir_to_target
+
+
+def standing_on_consumable(t):
+    """Return True/False whether t is standing on a consumable."""
+    for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
+               if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
+               if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
+               if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
+                          ["TT_CONSUMABLE"]]:
+        return True
+    return False
+
+
+def get_inventory_slot_to_consume(t):
+    """Return slot Id of strongest consumable in t's inventory, else -1."""
+    cmp_consumability = 0
+    selection = -1
+    i = 0
+    for id in t["T_CARRIES"]:
+        type = world_db["Things"][id]["T_TYPE"]
+        if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
+            cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
+            selection = i
+        i += 1
+    return selection
+
+
+def ai(t):
+    """Determine next command/argment for actor t via AI algorithms.
+
+    AI will look for, and move towards, enemies (animate Things not of their
+    own ThingType); if they see none, they will consume consumables in their
+    inventory; if there are none, they will pick up what they stand on if they
+    stand on consumables; if they stand on none, they will move towards the
+    next consumable they see or remember on the map; if they see or remember
+    none, they will explore parts of the map unseen since ever or for at least
+    one turn; if there is nothing to explore, they will simply wait.
+    """
+    t["T_COMMAND"] = [id for id in world_db["ThingActions"]
+                      if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
+    if not get_dir_to_target(t, "f"):
+        sel = get_inventory_slot_to_consume(t)
+        if -1 != sel:
+            t["T_COMMAND"] = [id for id in world_db["ThingActions"]
+                              if world_db["ThingActions"][id]["TA_NAME"]
+                                 == "use"][0]
+            t["T_ARGUMENT"] = sel
+        elif standing_on_consumable(t):
+            t["T_COMMAND"] = [id for id in world_db["ThingActions"]
+                              if world_db["ThingActions"][id]["TA_NAME"]
+                                 == "pick_up"][0]
+        elif (not get_dir_to_target(t, "c")) and \
+             (not get_dir_to_target(t, "a")):
+            get_dir_to_target(t, "s")
+
+
 def turn_over():
     """Run game world and its inhabitants until new player input expected."""
     id = 0
     whilebreaker = False
     while world_db["Things"][0]["T_LIFEPOINTS"]:
-        for id in [id for id in world_db["Things"]]:
+        for id in [id for id in world_db["Things"]]:  # Only what's from start!
+            if not id in world_db["Things"] or \
+               world_db["Things"][id]["carried"]:   # May have been consumed or
+                continue                            # picked up during turn …
             Thing = world_db["Things"][id]
             if Thing["T_LIFEPOINTS"]:
                 if not Thing["T_COMMAND"]:
@@ -759,8 +1018,7 @@ def turn_over():
                     if 0 == id:
                         whilebreaker = True
                         break
-                    # DUMMY: ai(thing)
-                    Thing["T_COMMAND"] = 1
+                    ai(Thing)
                 try_healing(Thing)
                 Thing["T_PROGRESS"] += 1
                 taid = [a for a in world_db["ThingActions"]
@@ -802,17 +1060,14 @@ def new_Thing(type, pos=(0, 0)):
 
 def id_setter(id, category, id_store=False, start_at_1=False):
     """Set ID of object of category to manipulate ID unused? Create new one.
-
     The ID is stored as id_store.id (if id_store is set). If the integer of the
-    input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
-    32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
-    >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
-    new object is created, otherwise the new object's ID.
+    input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
+    <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
+    always returned when no new object is created, else the new object's ID.
     """
-    min = 0 if start_at_1 else -32768
-    max = 255 if start_at_1 else 32767
+    min = 0 if start_at_1 else -1
     if str == type(id):
-        id = integer_test(id, min, max)
+        id = integer_test(id, min)
     if None != id:
         if id in world_db[category]:
             if id_store:
@@ -820,15 +1075,12 @@ def id_setter(id, category, id_store=False, start_at_1=False):
             return None
         else:
             if (start_at_1 and 0 == id) \
-               or ((not start_at_1) and (id < 0 or id > 255)):
-                id = -1
+               or ((not start_at_1) and (id < 0)):
+                id = 0 if start_at_1 else -1
                 while 1:
                     id = id + 1
                     if id not in world_db[category]:
                         break
-                if id > 255:
-                    print("Ignoring: "
-                          "No unused ID available to add to ID list.")
                     return None
             if id_store:
                 id_store.id = id
@@ -842,18 +1094,13 @@ def command_ping():
 
 def command_quit():
     """Abort server process."""
+    save_world()
+    atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
     raise SystemExit("received QUIT command")
 
 
 def command_thingshere(str_y, str_x):
     """Write to out file list of Things known to player at coordinate y, x."""
-    def write_thing_if_here():
-        if y == world_db["Things"][id]["T_POSY"] \
-           and x == world_db["Things"][id]["T_POSX"] \
-           and not world_db["Things"][id]["carried"]:
-            type = world_db["Things"][id]["T_TYPE"]
-            name = world_db["ThingTypes"][type]["TT_NAME"]
-            strong_write(io_db["file_out"], name + "\n")
     if world_db["WORLD_ACTIVE"]:
         y = integer_test(str_y, 0, 255)
         x = integer_test(str_x, 0, 255)
@@ -863,10 +1110,17 @@ def command_thingshere(str_y, str_x):
             strong_write(io_db["file_out"], "THINGS_HERE START\n")
             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
                 for id in world_db["Things"]:
-                    write_thing_if_here()
+                    if y == world_db["Things"][id]["T_POSY"] \
+                       and x == world_db["Things"][id]["T_POSX"] \
+                       and not world_db["Things"][id]["carried"]:
+                        type = world_db["Things"][id]["T_TYPE"]
+                        name = world_db["ThingTypes"][type]["TT_NAME"]
+                        strong_write(io_db["file_out"], name + "\n")
             else:
-                for id in world_db["Things"][0]["T_MEMTHING"]:
-                    write_thing_if_here()
+                for mt in world_db["Things"][0]["T_MEMTHING"]:
+                    if y == mt[1] and x == mt[2]:
+                        name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
+                        strong_write(io_db["file_out"], name + "\n")
             strong_write(io_db["file_out"], "THINGS_HERE END\n")
         else:
             print("Ignoring: Invalid map coordinates.")
@@ -894,7 +1148,7 @@ def play_commander(action, args=False):
 
     def set_command_and_argument_movestring(str_arg):
         if str_arg in directions_db:
-            world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
+            world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
             set_command()
         else:
             print("Ignoring: Argument must be valid direction string.")
@@ -1034,7 +1288,7 @@ def command_worldactive(worldactive_string):
                     if world_db["Things"][id]["T_LIFEPOINTS"]:
                         build_fov_map(world_db["Things"][id])
                         if 0 == id:
-                            update_map_memory(world_db["Things"][id])
+                            update_map_memory(world_db["Things"][id], False)
                 world_db["WORLD_ACTIVE"] = 1
 
 
@@ -1071,7 +1325,7 @@ test_Thing_id = test_for_id_maker(command_tid, "Thing")
 @test_Thing_id
 def command_tcommand(str_int):
     """Set T_COMMAND of selected Thing."""
-    val = integer_test(str_int, 0, 255)
+    val = integer_test(str_int, 0)
     if None != val:
         if 0 == val or val in world_db["ThingActions"]:
             world_db["Things"][command_tid.id]["T_COMMAND"] = val
@@ -1082,7 +1336,7 @@ def command_tcommand(str_int):
 @test_Thing_id
 def command_ttype(str_int):
     """Set T_TYPE of selected Thing."""
-    val = integer_test(str_int, 0, 255)
+    val = integer_test(str_int, 0)
     if None != val:
         if val in world_db["ThingTypes"]:
             world_db["Things"][command_tid.id]["T_TYPE"] = val
@@ -1097,7 +1351,7 @@ def command_tcarries(str_int):
     The ID int(str_int) must not be of the selected Thing, and must belong to a
     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
     """
-    val = integer_test(str_int, 0, 255)
+    val = integer_test(str_int, 0)
     if None != val:
         if val == command_tid.id:
             print("Ignoring: Thing cannot carry itself.")
@@ -1118,7 +1372,7 @@ def command_tmemthing(str_t, str_y, str_x):
 
     The type must fit to an existing ThingType, and the position into the map.
     """
-    type = integer_test(str_t, 0, 255)
+    type = integer_test(str_t, 0)
     posy = integer_test(str_y, 0, 255)
     posx = integer_test(str_x, 0, 255)
     if None != type and None != posy and None != posx:
@@ -1215,7 +1469,7 @@ def command_ttsymbol(char):
 @test_ThingType_id
 def command_ttcorpseid(str_int):
     """Set TT_CORPSE_ID of selected ThingType."""
-    val = integer_test(str_int, 0, 255)
+    val = integer_test(str_int, 0)
     if None != val:
         if val in world_db["ThingTypes"]:
             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
@@ -1262,6 +1516,12 @@ def command_taname(name):
     # In contrast to the original,naming won't map a function to a ThingAction.
 
 
+def command_ai():
+    """Call ai() on player Thing, then turn_over()."""
+    ai(world_db["Things"][0])
+    turn_over()
+
+
 """Commands database.
 
 Map command start tokens to ([0]) number of expected command arguments, ([1])
@@ -1277,7 +1537,7 @@ commands_db = {
     "SEED_MAP": (1, False, command_seedmap),
     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
-    "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
+    "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
     "MAP_LENGTH": (1, False, command_maplength),
     "WORLD_ACTIVE": (1, False, command_worldactive),
     "TA_ID": (1, False, command_taid),
@@ -1312,15 +1572,16 @@ commands_db = {
     "pick_up": (0, False, play_commander("pick_up")),
     "drop": (1, False, play_commander("drop", True)),
     "use": (1, False, play_commander("use", True)),
+    "ai": (0, False, command_ai)
 }
 
 
 """World state database. With sane default values. (Randomness is in rand.)"""
 world_db = {
     "TURN": 0,
+    "MAP_LENGTH": 64,
     "SEED_MAP": 0,
     "PLAYER_TYPE": 0,
-    "MAP_LENGTH": 64,
     "WORLD_ACTIVE": 0,
     "ThingActions": {},
     "ThingTypes": {},
@@ -1334,7 +1595,7 @@ directions_db = {"east": "d", "south-east": "c", "south-west": "x",
 """File IO database."""
 io_db = {
     "path_save": "save",
-    "path_record": "record",
+    "path_record": "record_save",
     "path_worldconf": "confserver/world",
     "path_server": "server/",
     "path_in": "server/in",
@@ -1350,7 +1611,12 @@ try:
     libpr = prep_library()
     rand = RandomnessIO()
     opts = parse_command_line_arguments()
+    if opts.savefile:
+        io_db["path_save"] = opts.savefile
+        io_db["path_record"] = "record_" + opts.savefile
     setup_server_io()
+    if opts.verbose:
+        io_db["verbose"] = True
     if None != opts.replay:
         replay_game()
     else: