X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=plomrogue-server.py;h=c6bdd44d2832817a93e1ef549c429eb00e3205d3;hb=e09a0c1881ced1ca2873c5f39791f6198004b4cd;hp=299155df632a0668125255ae09e97587066e364d;hpb=a9ae51ca55e7504fb84f19ca99e150ae01fbf9c6;p=plomrogue diff --git a/plomrogue-server.py b/plomrogue-server.py index 299155d..c6bdd44 100755 --- a/plomrogue-server.py +++ b/plomrogue-server.py @@ -4,6 +4,42 @@ import os import shlex import shutil import time +import ctypes + + +class RandomnessIO: + """"Interface to libplomrogue's pseudo-randomness generator.""" + + def set_seed(self, seed): + libpr.seed_rrand(1, seed) + + def get_seed(self): + return libpr.seed_rrand(0, 0) + + def next(self): + return libpr.rrand() + + seed = property(get_seed, set_seed) + + +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?") + 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"]) + return libpr def strong_write(file, string): @@ -182,7 +218,8 @@ def save_world(): string = string + "T_ID " + str(id) + "\n" for carried_id in world_db["Things"][id]["T_CARRIES"]: string = string + "T_CARRIES " + str(carried_id) + "\n" - string = string + "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"]) + string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \ + "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"]) atomic_write(io_db["path_save"], string) @@ -232,7 +269,7 @@ def read_command(): Keep building return string until a newline is encountered. Pause between unsuccessful reads, and after too much waiting, run server_test(). """ - wait_on_fail = 1 + wait_on_fail = 0.03333 max_wait = 5 now = time.time() command = "" @@ -365,8 +402,62 @@ def play_game(): def remake_map(): - # DUMMY map creator. - world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2)) + """(Re-)make island map. + + Let "~" represent water, "." land, "X" trees: Build island shape randomly, + start with one land cell in the middle, then go into cycle of repeatedly + selecting a random sea cell and transforming it into land if it is neighbor + to land. The cycle ends when a land cell is due to be created at the map's + border. Then put some trees on the map (TODO: more precise algorithm desc). + """ + def is_neighbor(coordinates, type): + y = coordinates[0] + x = coordinates[1] + length = world_db["MAP_LENGTH"] + ind = y % 2 + diag_west = x + (ind > 0) + diag_east = x + (ind < (length - 1)) + pos = (y * length) + x + if (y > 0 and diag_east + and type == chr(world_db["MAP"][pos - length + ind])) \ + or (x < (length - 1) + and type == chr(world_db["MAP"][pos + 1])) \ + or (y < (length - 1) and diag_east + and type == chr(world_db["MAP"][pos + length + ind])) \ + or (y > 0 and diag_west + and type == chr(world_db["MAP"][pos - length - (not ind)])) \ + or (x > 0 + and type == chr(world_db["MAP"][pos - 1])) \ + or (y < (length - 1) and diag_west + and type == chr(world_db["MAP"][pos + length - (not ind)])): + return True + return False + store_seed = rand.seed + world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2)) + length = world_db["MAP_LENGTH"] + add_half_width = (not (length % 2)) * int(length / 2) + world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".") + while (1): + y = rand.next() % length + x = rand.next() % length + pos = (y * length) + x + if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."): + if y == 0 or y == (length - 1) or x == 0 or x == (length - 1): + break + world_db["MAP"][pos] = ord(".") + n_trees = int((length ** 2) / 16) + i_trees = 0 + while (i_trees <= n_trees): + single_allowed = rand.next() % 32 + y = rand.next() % length + x = rand.next() % length + pos = (y * length) + x + if "." == chr(world_db["MAP"][pos]) \ + and ((not single_allowed) or is_neighbor((y, x), "X")): + world_db["MAP"][pos] = ord("X") + i_trees += 1 + rand.seed = store_seed + # This all-too-precise replica of the original C code misses iter_limit(). def update_map_memory(t): @@ -382,11 +473,11 @@ def update_map_memory(t): t["T_MEMMAP"][pos] = world_db["MAP"][pos] continue # TODO: Aging of MEMDEPTHMAP. - for id in t["T_MEMTHING"]: - y = world_db["Things"][id]["T_POSY"] - x = world_db["Things"][id]["T_POSY"] + 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(id) + t["T_MEMTHING"].remove(memthing) for id in world_db["Things"]: type = world_db["Things"][id]["T_TYPE"] if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]: @@ -449,7 +540,157 @@ def build_fov_map(t): # DUMMY so far. Just builds an all-visible map. -def new_Thing(type): +def decrement_lifepoints(t): + """Decrement t's lifepoints by 1, and if to zero, corpse it. + + If t is the player avatar, only blank its fovmap, so that the client may + still display memory data. On non-player things, erase fovmap and memory. + """ + t["T_LIFEPOINTS"] -= 1 + if 0 == t["T_LIFEPOINTS"]: + t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"] + if world_db["Things"][0] == t: + t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2)) + strong_write(io_db["file_out"], "LOG You die.\n") + else: + t["fovmap"] = False + t["T_MEMMAP"] = False + t["T_MEMDEPTHMAP"] = False + t["T_MEMTHING"] = [] + strong_write(io_db["file_out"], "LOG It dies.\n") + + +def actor_wait(t): + """Make t do nothing (but loudly, if player avatar).""" + if t == world_db["Things"][0]: + strong_write(io_db["file_out"], "LOG You wait.\n") + + +def actor_move(t): + """If passable, move/collide(=attack) thing into T_ARGUMENT's direction.""" + dir_c = t["T_ARGUMENT"].encode("ascii")[0] + legal_move = libpr.mv_yx_in_dir_legal_wrap(dir_c, t["T_POSY"], t["T_POSX"]) + passable = False + if -1 == legal_move: + raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!") + elif 1 == legal_move: + pos = (libpr.result_y() * world_db["MAP_LENGTH"]) + libpr.result_x() + passable = "." == chr(world_db["MAP"][pos]) + hitted = [id for id in world_db["Things"] + if world_db["Things"][id] != t + if world_db["Things"][id]["T_LIFEPOINTS"] + if world_db["Things"][id]["T_POSY"] == libpr.result_y() + if world_db["Things"][id]["T_POSX"] == libpr.result_x()] + if len(hitted): + hitted = hitted[0] + decrement_lifepoints(world_db["Things"][hitted]) + hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"] + hitter = "You" if t == world_db["Things"][0] else hitter_name + hitted_type = world_db["Things"][hitted]["T_TYPE"] + hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"] + hitted = "you" if hitted == world_db["Things"][0] else hitted_name + verb = " wound " if hitter == "You" else " wounds " + strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \ + ".\n") + return + dir = [dir for dir in directions_db + if directions_db[dir] == t["T_ARGUMENT"]][0] + if passable: + t["T_POSY"] = libpr.result_y() + t["T_POSX"] = libpr.result_x() + for id in t["T_CARRIES"]: + world_db["Things"][id]["T_POSY"] = libpr.result_y() + world_db["Things"][id]["T_POSX"] = libpr.result_x() + build_fov_map(t) + strong_write(io_db["file_out"], "LOG You move " + dir + ".\n") + else: + 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. + 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]) + 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]: + err = "You try to pick up an object, but there is none." + strong_write(io_db["file_out"], "LOG " + err + "\n") + + +def actor_drop(t): + """Make t rop Thing from inventory to ground indexed by T_ARGUMENT.""" + # TODO: Handle case where T_ARGUMENT matches nothing. + if len(t["T_CARRIES"]): + id = t["T_CARRIES"][t["T_ARGUMENT"]] + t["T_CARRIES"].remove(id) + world_db["Things"][id]["carried"] = False + if t == world_db["Things"][0]: + strong_write(io_db["file_out"], "LOG You drop an object.\n") + elif t == world_db["Things"][0]: + err = "You try to drop an object, but you own none." + strong_write(io_db["file_out"], "LOG " + err + "\n") + + +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"]] + type = world_db["Things"][id]["T_TYPE"] + if world_db["ThingTypes"][type]["TT_CONSUMABLE"]: + 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") + + +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"] + if world_db["Things"][id]["T_LIFEPOINTS"]]: + Thing = world_db["Things"][id] + if Thing["T_LIFEPOINTS"]: + if not Thing["T_COMMAND"]: + update_map_memory(Thing) + if 0 == id: + whilebreaker = True + break + # DUMMY: ai(thing) + Thing["T_COMMAND"] = 1 + # DUMMY: try_healing + Thing["T_PROGRESS"] += 1 + taid = [a for a in world_db["ThingActions"] + if a == Thing["T_COMMAND"]][0] + ThingAction = world_db["ThingActions"][taid] + if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]: + eval("actor_" + ThingAction["TA_NAME"])(Thing) + Thing["T_COMMAND"] = 0 + Thing["T_PROGRESS"] = 0 + # DUMMY: hunger + # DUMMY: thingproliferation + if whilebreaker: + break + world_db["TURN"] += 1 + + +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"], @@ -458,8 +699,8 @@ def new_Thing(type): "T_SATIATION": 0, "T_COMMAND": 0, "T_TYPE": type, - "T_POSY": 0, - "T_POSX": 0, + "T_POSY": pos[0], + "T_POSX": pos[1], "T_CARRIES": [], "carried": False, "T_MEMTHING": [], @@ -521,7 +762,8 @@ 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 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") @@ -545,6 +787,46 @@ def command_thingshere(str_y, str_x): print("Ignoring: Command only works on existing worlds.") +def play_commander(action, args=False): + """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over(). + + T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args. + """ + + def set_command(): + id = [x for x in world_db["ThingActions"] + if world_db["ThingActions"][x]["TA_NAME"] == action][0] + world_db["Things"][0]["T_COMMAND"] = id + turn_over() + + def set_command_and_argument_int(str_arg): + val = integer_test(str_arg, 0, 255) + if None != val: + world_db["Things"][0]["T_ARGUMENT"] = val + set_command() + + def set_command_and_argument_movestring(str_arg): + if str_arg in directions_db: + world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg] + set_command() + else: + print("Ignoring: Argument must be valid direction string.") + + if action == "move": + return set_command_and_argument_movestring + elif args: + return set_command_and_argument_int + else: + return set_command + + +def command_seedrandomness(seed_string): + """Set rand seed to int(seed_string).""" + val = integer_test(seed_string, 0, 4294967295) + if None != val: + rand.seed = val + + def command_seedmap(seed_string): """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map.""" setter(None, "SEED_MAP", 0, 4294967295)(seed_string) @@ -554,16 +836,41 @@ def command_seedmap(seed_string): def command_makeworld(seed_string): """(Re-)build game world, i.e. map, things, to a new turn 1 from seed. - Make seed world_db["SEED_RANDOMNESS"] and world_db["SEED_MAP"]. Do more - only with a "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType - of TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map() + Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a + "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of + TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map() and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each other. Init player's memory map. Write "NEW_WORLD" line to out file. """ - setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string) - setter(None, "SEED_MAP", 0, 4294967295)(seed_string) + + def free_pos(): + i = 0 + while 1: + err = "Space to put thing on too hard to find. Map too small?" + while 1: + y = rand.next() % world_db["MAP_LENGTH"] + x = rand.next() % world_db["MAP_LENGTH"] + if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]): + break + i += 1 + if i == 65535: + raise SystemExit(err) + # Replica of C code, wrongly ignores animatedness of new Thing. + pos_clear = (0 == len([id for id in world_db["Things"] + if world_db["Things"][id]["T_LIFEPOINTS"] + if world_db["Things"][id]["T_POSY"] == y + if world_db["Things"][id]["T_POSX"] == x])) + if pos_clear: + break + return (y, x) + + val = integer_test(seed_string, 0, 4294967295) + if None == val: + return + rand.seed = val + world_db["SEED_MAP"] = val player_will_be_generated = False playertype = world_db["PLAYER_TYPE"] for ThingType in world_db["ThingTypes"]: @@ -589,23 +896,24 @@ def command_makeworld(seed_string): world_db["TURN"] = 1 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]): id = id_setter(-1, "Things") - world_db["Things"][id] = new_Thing(playertype) - # TODO: Positioning. + world_db["Things"][id] = new_Thing(playertype, free_pos()) update_map_memory(world_db["Things"][0]) for type in world_db["ThingTypes"]: for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]): if type != playertype: id = id_setter(-1, "Things") - world_db["Things"][id] = new_Thing(type) - # TODO: Positioning. + world_db["Things"][id] = new_Thing(type, free_pos()) strong_write(io_db["file_out"], "NEW_WORLD\n") def command_maplength(maplength_string): """Redefine map length. Invalidate map, therefore lose all things on it.""" - set_world_inactive() - world_db["Things"] = {} - setter(None, "MAP_LENGTH", 1, 256)(maplength_string) + val = integer_test(maplength_string, 1, 256) + if None != val: + world_db["MAP_LENGTH"] = val + set_world_inactive() + world_db["Things"] = {} + libpr.set_maplength(val) def command_worldactive(worldactive_string): @@ -880,8 +1188,7 @@ commands_db = { "THINGS_HERE": (2, True, command_thingshere), "MAKE_WORLD": (1, False, command_makeworld), "SEED_MAP": (1, False, command_seedmap), - "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS", - 0, 4294967295)), + "SEED_RANDOMNESS": (1, False, command_seedrandomness), "TURN": (1, False, setter(None, "TURN", 0, 65535)), "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)), "MAP_LENGTH": (1, False, command_maplength), @@ -913,14 +1220,18 @@ commands_db = { "T_MEMTHING": (3, False, command_tmemthing), "T_POSY": (1, False, setter_tpos("Y")), "T_POSX": (1, False, setter_tpos("X")), + "wait": (0, False, play_commander("wait")), + "move": (1, False, play_commander("move")), + "pick_up": (0, False, play_commander("pick_up")), + "drop": (1, False, play_commander("drop", True)), + "use": (1, False, play_commander("use", True)), } -"""World state database. With sane default values.""" +"""World state database. With sane default values. (Randomness is in rand.)""" world_db = { "TURN": 0, "SEED_MAP": 0, - "SEED_RANDOMNESS": 0, "PLAYER_TYPE": 0, "MAP_LENGTH": 64, "WORLD_ACTIVE": 0, @@ -929,6 +1240,9 @@ world_db = { "Things": {} } +"""Mapping of direction names to internal direction chars.""" +directions_db = {"east": "d", "south-east": "c", "south-west": "x", + "west": "s", "north-west": "w", "north-east": "e"} """File IO database.""" io_db = { @@ -946,6 +1260,8 @@ io_db = { try: + libpr = prep_library() + rand = RandomnessIO() opts = parse_command_line_arguments() setup_server_io() if None != opts.replay: