home · contact · privacy
Server/py: Fix thing proliferation bug in turn_over().
[plomrogue] / plomrogue-server.py
index 59b1af3af34c5dcb0f45c14c6d2c6ec7ec4a6db4..5223e4bf0438945036378543b88e31926e7ed80d 100755 (executable)
@@ -204,8 +204,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"]:
@@ -341,12 +343,11 @@ def try_worldstate_update():
         string = write_map(string, fov)
         mem = world_db["Things"][0]["T_MEMMAP"][:]
         for i in range(2):
-            for memthing in world_db["Things"][0]["T_MEMTHING"]:
-                type = world_db["Things"][memthing[0]]["T_TYPE"]
-                consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
+            for mt in world_db["Things"][0]["T_MEMTHING"]:
+                consumable = world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]
                 if (i == 0 and not consumable) or (i == 1 and consumable):
-                    c = world_db["ThingTypes"][type]["TT_SYMBOL"]
-                    mem[(memthing[1] * length) + memthing[2]] = ord(c)
+                    c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
+                    mem[(mt[1] * length) + mt[2]] = ord(c)
         string = write_map(string, mem)
         atomic_write(io_db["path_worldstate"], string)
         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
@@ -436,6 +437,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)
@@ -463,7 +465,7 @@ 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))
@@ -475,13 +477,16 @@ def update_map_memory(t):
             if " " == chr(t["T_MEMMAP"][pos]):
                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
             continue
-        # TODO: Aging of MEMDEPTHMAP.
-    for memthing in t["T_MEMTHING"]:
-        y = world_db["Things"][memthing[0]]["T_POSY"]
-        x = world_db["Things"][memthing[0]]["T_POSX"]
-        if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
-            t["T_MEMTHING"].remove(memthing)
-    for id in world_db["Things"]:
+        if age_map and 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
+    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 [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"]
@@ -586,7 +591,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])
@@ -603,12 +609,12 @@ def actor_move(t):
             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
             hitted = "you" if hit_id == 0 else hitted_name
             verb = " wound " if hitter == "You" else " wounds "
-            strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
+            strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted +
                                             ".\n")
             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]
@@ -616,21 +622,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]:
@@ -654,7 +665,6 @@ def actor_drop(t):
 
 def actor_use(t):
     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
-    # Original wrongly featured lifepoints increase through consumable!
     # TODO: Handle case where T_ARGUMENT matches nothing.
     if len(t["T_CARRIES"]):
         id = t["T_CARRIES"][t["T_ARGUMENT"]]
@@ -663,13 +673,15 @@ def actor_use(t):
             t["T_CARRIES"].remove(id)
             del world_db["Things"][id]
             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
-            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):
@@ -686,7 +698,7 @@ def thingproliferation(t):
                        if y == world_db["Things"][id]["T_POSY"]
                        if x == world_db["Things"][id]["T_POSX"]
                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
-                       or (t["T_LIFEPOINTS"] and 
+                       or (t["T_LIFEPOINTS"] and
                            world_db["Things"][id]["T_LIFEPOINTS"])]:
                 return False
             return True
@@ -705,8 +717,29 @@ def thingproliferation(t):
             world_db["Things"][id] = newT
 
 
+def try_healing(t):
+    """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
+
+    On success, decrease satiation score by 32.
+    """
+    if t["T_SATIATION"] > 0 \
+       and t["T_LIFEPOINTS"] < \
+           world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
+       and 0 == (rand.next() % 31) \
+       and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
+                              if world_db["ThingActions"][id]["TA_NAME"] ==
+                                 "wait"][0]:
+        t["T_LIFEPOINTS"] += 1
+        t["T_SATIATION"] -= 32
+        if t == world_db["Things"][0]:
+            strong_write(io_db["file_out"], "LOG You heal.\n")
+        else:
+            name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
+            strong_write(io_db["file_out"], "LOG " + name + "heals.\n")
+
+
 def hunger(t):
-    """Decrement t's satiation, dependent on it trigger lifepoint dec chance."""
+    """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
     if t["T_SATIATION"] > -32768:
         t["T_SATIATION"] -= 1
     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
@@ -718,17 +751,79 @@ def hunger(t):
             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
         else:
             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
-            strong_write(io_db["file_out"], "LOG " + name + \
+            strong_write(io_db["file_out"], "LOG " + name +
                                             " suffers from hunger.\n")
         decrement_lifepoints(t)
 
 
+def get_dir_to_nearest_target(t, c):
+    # Dummy
+    return False
+
+
+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_nearest_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_nearest_target(t, "c")) and \
+             (not get_dir_to_nearest_target(t, "a")):
+            get_dir_to_nearest_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 is from start!
+            if not id in world_db["Things"] or \
+               world_db["Things"][id]["carried"]:# Thing may have been consumed
+                continue                         # or picked up during turn …
             Thing = world_db["Things"][id]
             if Thing["T_LIFEPOINTS"]:
                 if not Thing["T_COMMAND"]:
@@ -736,9 +831,9 @@ def turn_over():
                     if 0 == id:
                         whilebreaker = True
                         break
-                    # DUMMY: ai(thing)
+                    ai(Thing)
                     Thing["T_COMMAND"] = 1
-                # DUMMY: try_healing
+                try_healing(Thing)
                 Thing["T_PROGRESS"] += 1
                 taid = [a for a in world_db["ThingActions"]
                           if a == Thing["T_COMMAND"]][0]
@@ -754,7 +849,7 @@ def turn_over():
         world_db["TURN"] += 1
 
 
-def new_Thing(type, pos=(0,0)):
+def new_Thing(type, pos=(0, 0)):
     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
     thing = {
         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
@@ -824,13 +919,6 @@ def command_quit():
 
 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)
@@ -840,10 +928,18 @@ 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()
+                    # 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.")
@@ -871,7 +967,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.")
@@ -1239,6 +1335,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])
@@ -1289,15 +1391,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": {},