X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=plomrogue-server.py;h=2faf15ce64d8f06e9f8e2e4f6781a1fce03d618b;hb=a502fc715d081cb95b03a684d42a66485c5039cb;hp=fda9b29df5d5e42dfa9821f6f97b63ef885e4bcb;hpb=662f4a0dcde66d10c5676fa53b45e84a26437855;p=plomrogue diff --git a/plomrogue-server.py b/plomrogue-server.py index fda9b29..2faf15c 100755 --- a/plomrogue-server.py +++ b/plomrogue-server.py @@ -4,6 +4,45 @@ 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"]) + 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): @@ -182,7 +221,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 +272,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 +405,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): @@ -384,7 +478,7 @@ def update_map_memory(t): # TODO: Aging of MEMDEPTHMAP. for memthing in t["T_MEMTHING"]: y = world_db["Things"][memthing[0]]["T_POSY"] - x = world_db["Things"][memthing[1]]["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"]: @@ -446,7 +540,169 @@ def setter(category, key, min, max): def build_fov_map(t): """Build Thing's FOV map.""" t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2)) - # DUMMY so far. Just builds an all-visible map. + maptype = ctypes.c_char * len(world_db["MAP"]) + test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"], + maptype.from_buffer(t["fovmap"]), + maptype.from_buffer(world_db["MAP"])) + if test: + raise SystemExit("Malloc error in build_fov_Map().") + + +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 mv_yx_in_dir_legal(dir, y, x): + """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use.""" + dir_c = dir.encode("ascii")[0] + test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x) + if -1 == test: + raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!") + return (test, libpr.result_y(), libpr.result_x()) + + +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.""" + passable = False + move_result = mv_yx_in_dir_legal(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]) + 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"] == move_result[1] + if world_db["Things"][id]["T_POSX"] == move_result[2]] + if len(hitted): + hit_id = hitted[0] + 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"][hit_id]["T_TYPE"] + 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 + \ + ".\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 passable: + t["T_POSY"] = move_result[1] + t["T_POSX"] = move_result[2] + for id in t["T_CARRIES"]: + 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: + 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 thingproliferation(t): + """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell. + + Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be + passable and not be inhabited by a Thing of the same type, or, if Thing is + animate, any other animate Thing. If there are several map cell candidates, + one is selected randomly. + """ + def test_cell(t, y, x): + if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]): + for id in [id for id in world_db["Things"] + 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 + world_db["Things"][id]["T_LIFEPOINTS"])]: + return False + return True + return False + prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"] + if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)): + candidates = [] + for dir in [directions_db[key] for key in directions_db]: + mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"]) + if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]): + candidates.append((mv_result[1], mv_result[2])) + if len(candidates): + i = rand.next() % len(candidates) + id = id_setter(-1, "Things") + newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1])) + world_db["Things"][id] = newT def turn_over(): @@ -454,9 +710,7 @@ def turn_over(): id = 0 whilebreaker = False while world_db["Things"][0]["T_LIFEPOINTS"]: - for id in [id for id in world_db["Things"] - if not world_db["Things"][id]["carried"] - if world_db["Things"][id]["T_LIFEPOINTS"]]: + for id in [id for id in world_db["Things"]]: Thing = world_db["Things"][id] if Thing["T_LIFEPOINTS"]: if not Thing["T_COMMAND"]: @@ -472,17 +726,17 @@ def turn_over(): if a == Thing["T_COMMAND"]][0] ThingAction = world_db["ThingActions"][taid] if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]: - # run_thing_action(action["TA_NAME"]) + eval("actor_" + ThingAction["TA_NAME"])(Thing) Thing["T_COMMAND"] = 0 Thing["T_PROGRESS"] = 0 # DUMMY: hunger - # DUMMY: thingproliferation + thingproliferation(Thing) if whilebreaker: break world_db["TURN"] += 1 -def new_Thing(type): +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"], @@ -491,8 +745,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": [], @@ -554,7 +808,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") @@ -589,21 +844,16 @@ def play_commander(action, args=False): if world_db["ThingActions"][x]["TA_NAME"] == action][0] world_db["Things"][0]["T_COMMAND"] = id turn_over() - # TODO: call 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() - else: - print("Ignoring: Argument must be integer >= 0 <=255.") def set_command_and_argument_movestring(str_arg): - dirs = {"east": "d", "south-east": "c", "south-west": "x", - "west": "s", "north-west": "w", "north-east": "e"} - if str_arg in dirs: - world_db["Things"][0]["T_ARGUMENT"] = dirs[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.") @@ -616,6 +866,13 @@ def play_commander(action, args=False): 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) @@ -625,16 +882,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"]: @@ -660,23 +942,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): @@ -951,8 +1234,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), @@ -992,11 +1274,10 @@ commands_db = { } -"""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, @@ -1005,6 +1286,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 = { @@ -1022,6 +1306,8 @@ io_db = { try: + libpr = prep_library() + rand = RandomnessIO() opts = parse_command_line_arguments() setup_server_io() if None != opts.replay: