X-Git-Url: https://plomlompom.com/repos/?p=plomrogue2-experiments;a=blobdiff_plain;f=new%2Fplomrogue%2Fgame.py;h=d20713cd61fa219d387808ef3c2e9bdbd8ac4fff;hp=071290019a4c2f58f552bdbf51f28b66887cb1da;hb=8f2dc382612c0684fd9a75e60c23561a1859cb8f;hpb=d84b39d4b7265d1ce6597023aedf6dca89836eea diff --git a/new/plomrogue/game.py b/new/plomrogue/game.py index 0712900..d20713c 100755 --- a/new/plomrogue/game.py +++ b/new/plomrogue/game.py @@ -1,285 +1,140 @@ -from plomrogue.tasks import Task_WAIT, Task_MOVE -from plomrogue.errors import GameError, ArgError -from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE, cmd_MAP, - cmd_MAP, cmd_THING_TYPE, cmd_THING_POS, - cmd_TERRAIN_LINE, cmd_PLAYER_ID, cmd_TURN, - cmd_SWITCH_PLAYER, cmd_SAVE) -from plomrogue.mapping import MapHex +from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_PICKUP, + Task_DROP, Task_EAT) +from plomrogue.errors import ArgError, GameError +from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE, + cmd_MAP, cmd_MAP, cmd_THING_TYPE, + cmd_THING_POS, cmd_THING_INVENTORY, + cmd_THING_HEALTH, cmd_SEED, + cmd_GET_PICKABLE_ITEMS, cmd_MAP_SIZE, + cmd_TERRAIN_LINE, cmd_PLAYER_ID, + cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE) +from plomrogue.mapping import MapGeometryHex, MapChunk, YX from plomrogue.parser import Parser from plomrogue.io import GameIO -from plomrogue.misc import quote, stringify_yx +from plomrogue.misc import quote +from plomrogue.things import (Thing, ThingMonster, ThingHuman, ThingFood, + ThingAnimate) +import random -class ThingBase: +class PRNGod(random.Random): - def __init__(self, world, id_, type_='?', position=[0,0]): - self.world = world - self.id_ = id_ - self.type_ = type_ - self.position = position + def seed(self, seed): + self.prngod_seed = seed + def getstate(self): + return self.prngod_seed + def setstate(seed): + self.seed(seed) -class Thing(ThingBase): + def random(self): + self.prngod_seed = ((self.prngod_seed * 1103515245) + 12345) % 2**32 + return (self.prngod_seed >> 16) / (2**16 - 1) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_task('WAIT') - self._last_task_result = None - self._stencil = None - - def move_towards_target(self, target): - dijkstra_map = type(self.world.map_)(self.world.map_.size) - n_max = 256 - dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)] - dijkstra_map[target] = 0 - shrunk = True - visible_map = self.get_visible_map() - while shrunk: - shrunk = False - for pos in dijkstra_map: - if visible_map[pos] != '.': - continue - neighbors = dijkstra_map.get_neighbors(tuple(pos)) - for direction in neighbors: - yx = neighbors[direction] - if yx is not None and dijkstra_map[yx] < dijkstra_map[pos] - 1: - dijkstra_map[pos] = dijkstra_map[yx] + 1 - shrunk = True - #with open('log', 'a') as f: - # f.write('---------------------------------\n') - # for y, line in dijkstra_map.lines(): - # for val in line: - # if val < 10: - # f.write(str(val)) - # elif val == 256: - # f.write('x') - # else: - # f.write('~') - # f.write('\n') - neighbors = dijkstra_map.get_neighbors(tuple(self.position)) - n = n_max - #print('DEBUG', self.position, neighbors) - #dirs = dijkstra_map.get_directions() - #print('DEBUG dirs', dirs) - #print('DEBUG neighbors', neighbors) - #debug_scores = [] - #for pos in neighbors: - # if pos is None: - # debug_scores += [9000] - # else: - # debug_scores += [dijkstra_map[pos]] - #print('DEBUG debug_scores', debug_scores) - target_direction = None - for direction in neighbors: - yx = neighbors[direction] - if yx is not None: - n_new = dijkstra_map[yx] - if n_new < n: - n = n_new - target_direction = direction - #print('DEBUG result', direction) - if target_direction: - self.set_task('MOVE', (target_direction,)) - - def decide_task(self): - # TODO: Check if monster can follow player too well (even when they should lose them) - visible_things = self.get_visible_things() - target = None - for t in visible_things: - if t.type_ == 'human': - target = t.position - break - if target is not None: - try: - self.move_towards_target(target) - return - except GameError: - pass - self.set_task('WAIT') - - def set_task(self, task_name, args=()): - task_class = self.world.game.tasks[task_name] - self.task = task_class(self, args) - self.task.check() # will throw GameError if necessary - - def proceed(self, is_AI=True): - """Further the thing in its tasks. - - Decrements .task.todo; if it thus falls to <= 0, enacts method - whose name is 'task_' + self.task.name and sets .task = - None. If is_AI, calls .decide_task to decide a self.task. - - Before doing anything, ensures an empty map visibility stencil - and checks that task is still possible, and aborts it - otherwise (for AI things, decides a new task). - """ - self._stencil = None - try: - self.task.check() - except GameError as e: - self.task = None - self._last_task_result = e - if is_AI: - try: - self.decide_task() - except GameError: - self.set_task('WAIT') - return - self.task.todo -= 1 - if self.task.todo <= 0: - self._last_task_result = self.task.do() - self.task = None - if is_AI and self.task is None: - try: - self.decide_task() - except GameError: - self.set_task('WAIT') - - def get_stencil(self): - if self._stencil is not None: - return self._stencil - self._stencil = self.world.map_.get_fov_map(self.position) - return self._stencil - - def get_visible_map(self): - stencil = self.get_stencil() - m = self.world.map_.new_from_shape(' ') - for pos in m: - if stencil[pos] == '.': - m[pos] = self.world.map_[pos] - return m - - def get_visible_things(self): - stencil = self.get_stencil() - visible_things = [] - for thing in self.world.things: - if stencil[thing.position] == '.': - visible_things += [thing] - return visible_things - - - -class WorldBase: - - def __init__(self, game): + +class GameBase: + + def __init__(self): self.turn = 0 self.things = [] - self.game = game def get_thing(self, id_, create_unfound=True): for thing in self.things: if id_ == thing.id_: return thing if create_unfound: - t = self.game.thing_type(self, id_) + t = self.thing_type(self, id_) self.things += [t] return t return None - - -class World(WorldBase): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.player_id = 0 - - def new_map(self, yx): - self.map_ = self.game.map_type(yx) - - def proceed_to_next_player_turn(self): - """Run game world turns until player can decide their next step. - - Iterates through all non-player things, on each step - furthering them in their tasks (and letting them decide new - ones if they finish). The iteration order is: first all things - that come after the player in the world things list, then - (after incrementing the world turn) all that come before the - player; then the player's .proceed() is run, and if it does - not finish his task, the loop starts at the beginning. Once - the player's task is finished, the loop breaks. - """ - while True: - player = self.get_player() - player_i = self.things.index(player) - for thing in self.things[player_i+1:]: - thing.proceed() - self.turn += 1 - for thing in self.things[:player_i]: - thing.proceed() - player.proceed(is_AI=False) - if player.task is None: - break - - def get_player(self): - return self.get_thing(self.player_id) - - def make_new(self, yx, seed): - import random - random.seed(seed) - self.turn = 0 - self.new_map(yx) - for pos in self.map_: - if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]: - self.map_[pos] = '#' - continue - self.map_[pos] = random.choice(('.', '.', '.', '.', 'x')) - player = self.game.thing_type(self, 0) - player.type_ = 'human' - player.position = [random.randint(0, yx[0] -1), - random.randint(0, yx[1] - 1)] - npc = self.game.thing_type(self, 1) - npc.type_ = 'monster' - npc.position = [random.randint(0, yx[0] -1), - random.randint(0, yx[1] -1)] - self.things = [player, npc] - return 'success' + def things_at_pos(self, pos): + things = [] + for t in self.things: + if t.position == pos: + things += [t] + return things -class Game: +class Game(GameBase): - def __init__(self, game_file_name): + def __init__(self, game_file_name, *args, **kwargs): + super().__init__(*args, **kwargs) self.io = GameIO(game_file_name, self) - self.map_type = MapHex - self.tasks = {'WAIT': Task_WAIT, 'MOVE': Task_MOVE} + self.map_size = None + self.map_geometry = MapGeometryHex() + self.tasks = {'WAIT': Task_WAIT, + 'MOVE': Task_MOVE, + 'PICKUP': Task_PICKUP, + 'EAT': Task_EAT, + 'DROP': Task_DROP} self.commands = {'GEN_WORLD': cmd_GEN_WORLD, 'GET_GAMESTATE': cmd_GET_GAMESTATE, + 'SEED': cmd_SEED, + 'MAP_SIZE': cmd_MAP_SIZE, 'MAP': cmd_MAP, 'THING_TYPE': cmd_THING_TYPE, 'THING_POS': cmd_THING_POS, + 'THING_HEALTH': cmd_THING_HEALTH, + 'THING_INVENTORY': cmd_THING_INVENTORY, 'TERRAIN_LINE': cmd_TERRAIN_LINE, + 'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS, 'PLAYER_ID': cmd_PLAYER_ID, 'TURN': cmd_TURN, 'SWITCH_PLAYER': cmd_SWITCH_PLAYER, 'SAVE': cmd_SAVE} - self.world_type = World - self.world = self.world_type(self) self.thing_type = Thing + self.thing_types = {'human': ThingHuman, + 'monster': ThingMonster, + 'food': ThingFood} + self.player_id = 0 + self.player_is_alive = True + self.maps = {} + self.max_map_awakeness = 100 + self.rand = PRNGod(0) def get_string_options(self, string_option_type): if string_option_type == 'direction': - return self.world.map_.get_directions() + return self.map_geometry.get_directions() + elif string_option_type == 'thingtype': + return list(self.thing_types.keys()) return None def send_gamestate(self, connection_id=None): """Send out game state data relevant to clients.""" - self.io.send('TURN ' + str(self.world.turn)) - self.io.send('MAP ' + stringify_yx(self.world.map_.size)) - visible_map = self.world.get_player().get_visible_map() + def send_thing(thing): + view_pos = self.map_geometry.pos_in_view(thing.position, + self.player.view_offset, + self.map_size) + self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_)) + self.io.send('THING_POS %s %s' % (thing.id_, view_pos)) + + self.io.send('PLAYER_ID ' + str(self.player_id)) + self.io.send('TURN ' + str(self.turn)) + visible_map = self.player.get_visible_map() + self.io.send('VISIBLE_MAP %s %s' % (visible_map.size, + visible_map.start_indented)) for y, line in visible_map.lines(): self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line))) - visible_things = self.world.get_player().get_visible_things() + visible_things = self.player.get_visible_things() for thing in visible_things: - self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_)) - self.io.send('THING_POS %s %s' % (thing.id_, - stringify_yx(thing.position))) - player = self.world.get_player() - self.io.send('PLAYER_POS %s' % (stringify_yx(player.position))) + send_thing(thing) + if hasattr(thing, 'health'): + self.io.send('THING_HEALTH %s %s' % (thing.id_, + thing.health)) + if len(self.player.inventory) > 0: + self.io.send('PLAYER_INVENTORY %s' % + ','.join([str(i) for i in self.player.inventory])) + else: + self.io.send('PLAYER_INVENTORY ,') + for id_ in self.player.inventory: + thing = self.get_thing(id_) + send_thing(thing) self.io.send('GAME_STATE_COMPLETE') def proceed(self): @@ -288,9 +143,9 @@ class Game: First sends 'TURN_FINISHED' message, then runs game world until new player input is needed, then sends game state. """ - self.io.send('TURN_FINISHED ' + str(self.world.turn)) - self.world.proceed_to_next_player_turn() - msg = str(self.world.get_player()._last_task_result) + self.io.send('TURN_FINISHED ' + str(self.turn)) + self.proceed_to_next_player_turn() + msg = str(self.player._last_task_result) self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg)) self.send_gamestate() @@ -303,11 +158,13 @@ class Game: return p def cmd_TASK_colon(task_name, game, *args): - game.world.get_player().set_task(task_name, args) + if not game.player_is_alive: + raise GameError('You are dead.') + game.player.set_task(task_name, args) game.proceed() def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args): - t = game.world.get_thing(thing_id, False) + t = game.get_thing(thing_id, False) if t is None: raise ArgError('No such Thing.') task_class = game.tasks[task_name] @@ -339,3 +196,165 @@ class Game: f = partial_with_attrs(self.commands[command_name], self) return f return None + + @property + def player(self): + return self.get_thing(self.player_id) + + def new_thing_id(self): + if len(self.things) == 0: + return 0 + return self.things[-1].id_ + 1 + + def get_map(self, map_pos): + if not (map_pos in self.maps and + self.maps[map_pos].size == self.map_size): + self.maps[map_pos] = MapChunk(self.map_size) + self.maps[map_pos].awake = self.max_map_awakeness + for pos in self.maps[map_pos]: + self.maps[map_pos][pos] = '.' + return self.maps[map_pos] + + def proceed_to_next_player_turn(self): + """Run game world turns until player can decide their next step. + + All things and processes inside the player's reality bubble + are worked through. Things are furthered in their tasks and, + if finished, decide new ones. The iteration order is: first + all things that come after the player in the world things + list, then (after incrementing the world turn) all that come + before the player; then the player's .proceed() is run. + + Next, parts of the game world are put to sleep or woken up + based on how close they are to the player's position, or how + short ago the player visited them. + + If the player's last task is finished at the end of the loop, + it breaks; otherwise it starts again. + + """ + + def proceed_world(): + for thing in self.things[player_i+1:]: + thing.proceed() + self.turn += 1 + for map_pos in self.maps: + if self.maps[map_pos].awake: + for pos in self.maps[map_pos]: + if self.rand.random() > 0.999 and \ + self.maps[map_pos][pos] == '.' and \ + len(self.things_at_pos((map_pos, pos))) == 0: + self.add_thing_at('food', (map_pos, pos)) + for thing in self.things[:player_i]: + thing.proceed() + self.player.proceed(is_AI=False) + + def reality_bubble(): + + def regenerate_chunk_from_map_stats(map_): + import math + max_stat = self.max_map_awakeness + for t_type in map_.stats: + stat = map_.stats[t_type] + to_create = stat['population'] // max_stat + mod_created = int(self.rand.randint(0, max_stat - 1) < + (stat['population'] % max_stat)) + to_create = (stat['population'] // max_stat) + mod_created + if to_create == 0: + continue + average_health = None + if stat['health'] > 0: + average_health = math.ceil(stat['health'] / + stat['population']) + for i in range(to_create): + t = self.add_thing_at_random(map_pos, t_type) + if average_health: + t.health = average_health + #if hasattr(t, 'health'): + # print('DEBUG create', t.type_, t.health) + + for map_pos in self.maps: + m = self.maps[map_pos] + if map_pos in self.player.close_maps: + + # Newly inside chunks are regenerated from .stats. + if not m.awake: + #print('DEBUG regen stats', map_pos, m.stats) + regenerate_chunk_from_map_stats(m) + + # Inside chunks are set to max .awake and don't collect + # stats. + m.awake = self.max_map_awakeness + m.stats = {} + + # Outside chunks grow distant through .awake decremention. + # They collect .stats until they fall asleep – then any things + # inside are disappeared. + elif m.awake > 0: + m.awake -= 1 + for t in self.things: + if t.position[0] == map_pos: + if not t.type_ in m.stats: + m.stats[t.type_] = {'population': 0, + 'health': 0} + m.stats[t.type_]['population'] += 1 + if isinstance(t, ThingAnimate): + m.stats[t.type_]['health'] += t.health + if not m.awake: + del self.things[self.things.index(t)] + #if not m.awake: + # print('DEBUG sleep stats', map_pos, m.stats) + + while True: + player_i = self.things.index(self.player) + proceed_world() + reality_bubble() + if self.player.task is None or not self.player_is_alive: + break + + def add_thing_at(self, type_, pos): + t = self.thing_types[type_](self) + t.position = pos + self.things += [t] + return t + + def add_thing_at_random(self, big_yx, type_): + while True: + new_pos = (big_yx, + YX(self.rand.randint(0, self.map_size.y - 1), + self.rand.randint(0, self.map_size.x - 1))) + if self.maps[new_pos[0]][new_pos[1]] != '.': + continue + if len(self.things_at_pos(new_pos)) > 0: + continue + return self.add_thing_at(type_, new_pos) + + def make_map_chunk(self, big_yx): + map_ = self.get_map(big_yx) + for pos in map_: + map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x')) + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'monster') + self.add_thing_at_random(big_yx, 'food') + self.add_thing_at_random(big_yx, 'food') + self.add_thing_at_random(big_yx, 'food') + self.add_thing_at_random(big_yx, 'food') + + def make_new_world(self, size, seed): + self.things = [] + self.rand.seed(seed) + self.turn = 0 + self.maps = {} + self.map_size = size + self.make_map_chunk(YX(0,0)) + player = self.add_thing_at_random(YX(0,0), 'human') + player.surroundings # To help initializing reality bubble, see + # comment on ThingAnimate._position_set + self.player_id = player.id_ + return 'success'