From 569bb0b9683cfd5db1fa100e49127fe84b39f0ac Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Fri, 20 Dec 2019 05:24:56 +0100 Subject: [PATCH] Add basic reality bubble mechanism. --- new/plomrogue/commands.py | 25 ++++-- new/plomrogue/game.py | 163 ++++++++++++++++++++++++++------------ new/plomrogue/mapping.py | 11 ++- new/plomrogue/things.py | 22 ++++- 4 files changed, 161 insertions(+), 60 deletions(-) diff --git a/new/plomrogue/commands.py b/new/plomrogue/commands.py index 744d471..c09bc4e 100644 --- a/new/plomrogue/commands.py +++ b/new/plomrogue/commands.py @@ -18,10 +18,20 @@ def cmd_MAP_SIZE(game, size): game.map_size = size cmd_MAP_SIZE.argtypes = 'yx_tuple:pos' -def cmd_MAP(game, map_pos): - """Ensure (possibly empty/'?'-filled) map at position map_pos.""" - game.get_map(map_pos) -cmd_MAP.argtypes = 'yx_tuple' +def cmd_MAP(game, map_pos, awakeness): + """Ensure (possibly empty/'?'-filled) map at position map_pos. + + Awakeness > 0 puts the map into the player's reality bubble. + + """ + m = game.get_map(map_pos) + m.awake = awakeness +cmd_MAP.argtypes = 'yx_tuple int:nonneg' + +def cmd_MAP_STATS(game, map_pos, type_, population, health): + m = game.get_map(map_pos) + m.stats[type_] = {'population': population, 'health': health} +cmd_MAP_STATS = 'yx_tuple string:thingtype int:nonneg int:nonneg' def cmd_THING_TYPE(game, i, type_): t_old = game.get_thing(i) @@ -106,7 +116,12 @@ def cmd_SAVE(game): write(f, 'SEED %s' % game.rand.prngod_seed) write(f, 'MAP_SIZE %s' % (game.map_size,)) for map_pos in game.maps: - write(f, 'MAP %s' % (map_pos,)) + m = game.maps[map_pos] + write(f, 'MAP %s %s' % (map_pos, m.awake)) + for t_type in m.stats: + write(f, 'MAP_STATS %s %s %s %s' % + (map_pos, t_type, m.stats[t_type]['population'], + m.stats[t_type]['health'])) for map_pos in game.maps: for y, line in game.maps[map_pos].lines(): write(f, 'TERRAIN_LINE %s %5s %s' % (map_pos, y, quote(line))) diff --git a/new/plomrogue/game.py b/new/plomrogue/game.py index 13b4002..0e1ff9a 100755 --- a/new/plomrogue/game.py +++ b/new/plomrogue/game.py @@ -12,7 +12,8 @@ from plomrogue.mapping import MapGeometryHex, Map, YX from plomrogue.parser import Parser from plomrogue.io import GameIO from plomrogue.misc import quote -from plomrogue.things import Thing, ThingMonster, ThingHuman, ThingFood +from plomrogue.things import (Thing, ThingMonster, ThingHuman, ThingFood, + ThingAnimate) import random @@ -112,6 +113,7 @@ class Game(GameBase): 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, @@ -203,44 +205,98 @@ class Game(GameBase): return 0 return self.things[-1].id_ + 1 - def get_map(self, map_pos, create_unfound=True): + def get_map(self, map_pos): if not (map_pos in self.maps and self.maps[map_pos].size == self.map_size): - if create_unfound: - self.maps[map_pos] = Map(self.map_size) - for pos in self.maps[map_pos]: - self.maps[map_pos][pos] = '.' - else: - return None + self.maps[map_pos] = Map(self.map_size) + 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. - 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, or the player is dead, the loop - breaks. + 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. """ - while True: - player_i = self.things.index(self.player) + + def proceed_world(): for thing in self.things[player_i+1:]: thing.proceed() self.turn += 1 - for pos in self.maps[YX(0,0)]: - if self.maps[YX(0,0)][pos] == '.' and \ - len(self.things_at_pos((YX(0,0), pos))) == 0 and \ - self.rand.random() > 0.999: - self.add_thing_at('food', (YX(0,0), pos)) + 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(): + import math + 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: + for t_type in m.stats: + stat = m.stats[t_type] + to_create = stat['population'] // 100 + to_create = stat['population'] // 100 +\ + int(self.rand.randint(0, 99) < (stat['population'] % 100)) + 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 + + # Inside chunks are set to max .awake and don't collect + # stats. + m.awake = 100 + 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: + if not m.awake: + del self.things[self.things.index(t)] + + 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 @@ -250,34 +306,43 @@ class Game(GameBase): self.things += [t] return t - def make_new_world(self, yx, seed): - - def add_thing_at_random(type_): - while True: - new_pos = (YX(0,0), - YX(self.rand.randint(0, yx.y - 1), - self.rand.randint(0, yx.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 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 = yx - map_ = self.get_map(YX(0,0)) - for pos in map_: - map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x')) - player = add_thing_at_random('human') + 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_ - add_thing_at_random('monster') - add_thing_at_random('monster') - add_thing_at_random('food') - add_thing_at_random('food') - add_thing_at_random('food') - add_thing_at_random('food') return 'success' - diff --git a/new/plomrogue/mapping.py b/new/plomrogue/mapping.py index 4c7658d..3cfe022 100644 --- a/new/plomrogue/mapping.py +++ b/new/plomrogue/mapping.py @@ -22,6 +22,8 @@ class Map: self.size = size self.terrain = init_char*self.size_i self.start_indented = start_indented + self.awake = 100 # asleep if zero + self.stats = {} def __getitem__(self, yx): return self.terrain[self.get_position_index(yx)] @@ -101,17 +103,20 @@ class MapGeometry(): def pos_in_view(self, pos, offset, maps_size): return self.undouble_coordinate(maps_size, pos) - offset - def get_view(self, maps_size, get_map, radius, view_offset): + def get_view_and_seen_maps(self, maps_size, get_map, radius, view_offset): m = Map(size=YX(radius*2+1, radius*2+1), start_indented=(view_offset.y % 2 == 0)) + seen_maps = [] for pos in m: seen_pos = self.correct_double_coordinate(maps_size, (0,0), pos + view_offset) - seen_map = get_map(seen_pos[0], False) + if seen_pos[0] not in seen_maps: + seen_maps += [seen_pos[0]] + seen_map = get_map(seen_pos[0]) if seen_map is None: seen_map = Map(size=maps_size) m[pos] = seen_map[seen_pos[1]] - return m + return m, seen_maps def correct_double_coordinate(self, map_size, big_yx, little_yx): diff --git a/new/plomrogue/things.py b/new/plomrogue/things.py index f300148..797fd6b 100644 --- a/new/plomrogue/things.py +++ b/new/plomrogue/things.py @@ -110,6 +110,19 @@ class ThingAnimate(Thing): self.set_task('WAIT') self._last_task_result = None self.unset_surroundings() + self.close_maps = () + + def _position_set(self, pos): + """For player we need to update .close_maps on every move via the + self.surroundings property method, to keep their reality + bubble in sync with their movement. + + """ + super()._position_set(pos) + if self.id_ == self.game.player_id: + if not hasattr(self, '_surroundings'): + self._surroundings = None + self.surroundings def move_on_dijkstra_map(self, own_pos, targets): visible_map = self.get_visible_map() @@ -270,9 +283,12 @@ class ThingAnimate(Thing): def surroundings(self): if self._surroundings is not None: return self._surroundings - s = self.game.map_geometry.get_view(self.game.map_size, - self.game.get_map, - self._radius, self.view_offset) + s, close_maps = self.\ + game.map_geometry.get_view_and_seen_maps(self.game.map_size, + self.game.get_map, + self._radius, + self.view_offset) + self.close_maps = close_maps self._surroundings = s return self._surroundings -- 2.30.2