From 97049163acc0844e1656df3f761c9ca612afdeb0 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 30 Apr 2019 05:04:07 +0200 Subject: [PATCH] Allow player movement beyond central map. Lots of mapping rewrite. --- new/example_client.py | 24 ++++---- new/plomrogue/commands.py | 4 +- new/plomrogue/game.py | 22 ++++---- new/plomrogue/mapping.py | 85 +++++++++++++++++------------ new/plomrogue/tasks.py | 14 ++--- new/plomrogue/things.py | 112 +++++++++++++++----------------------- 6 files changed, 128 insertions(+), 133 deletions(-) diff --git a/new/example_client.py b/new/example_client.py index 230b045..0ea2db6 100755 --- a/new/example_client.py +++ b/new/example_client.py @@ -5,14 +5,14 @@ import threading from plomrogue.parser import ArgError, Parser from plomrogue.commands import cmd_PLAYER_ID, cmd_THING_HEALTH from plomrogue.game import Game, WorldBase -from plomrogue.mapping import MapHex, YX +from plomrogue.mapping import Map, MapGeometryHex, YX from plomrogue.io import PlomSocket from plomrogue.things import ThingBase import types import queue -class ClientMap(MapHex): +class ClientMap(Map): def y_cut(self, map_lines, center_y, view_height): map_height = len(map_lines) @@ -62,9 +62,9 @@ class ClientMap(MapHex): else: for i in range(len(map_lines)): map_lines[i] = '0' + map_lines[i] - self.y_cut(map_lines, center.y, size.y) + self.y_cut(map_lines, center[1].y, size.y) map_width = self.size.x * 2 + 1 - self.x_cut(map_lines, center.x * 2, size.x, map_width) + self.x_cut(map_lines, center[1].x * 2, size.x, map_width) return map_lines @@ -136,7 +136,7 @@ cmd_THING_TYPE.argtypes = 'int:nonneg string' def cmd_THING_POS(game, i, yx): t = game.world.get_thing(i) - t.position = yx + t.position = YX(0,0), yx cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg' @@ -157,6 +157,7 @@ class Game: def __init__(self): self.parser = Parser(self) self.world = World(self) + self.map_geometry = MapGeometryHex() self.thing_type = ThingBase self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT, 'TURN_FINISHED': cmd_TURN_FINISHED, @@ -203,8 +204,6 @@ class Game: def log(self, msg): """Prefix msg plus newline to self.log_text.""" self.log_text = msg + '\n' + self.log_text - with open('log', 'w') as f: - f.write(self.log_text) self.tui.to_update['log'] = True def symbol_for_type(self, type_): @@ -338,7 +337,7 @@ class DescriptorWidget(TextLinesWidget): def get_text_lines(self): lines = [] pos_i = self.tui.game.world.map_.\ - get_position_index(self.tui.examiner_position) + get_position_index(self.tui.examiner_position[1]) terrain = self.tui.game.world.map_.terrain[pos_i] lines = [terrain] for t in self.tui.game.world.things_at_pos(self.tui.examiner_position): @@ -408,7 +407,7 @@ class MapWidget(Widget): if t.id_ in self.tui.game.world.player_inventory: continue pos_i = self.tui.game.world.map_.\ - get_position_index(t.position) + get_position_index(t.position[1]) symbol = self.tui.game.symbol_for_type(t.type_) if terrain_as_list[pos_i][0] in {'f', '@', 'm'}: old_symbol = terrain_as_list[pos_i][0] @@ -419,7 +418,7 @@ class MapWidget(Widget): terrain_as_list[pos_i] = symbol if self.tui.examiner_mode: pos_i = self.tui.game.world.map_.\ - get_position_index(self.tui.examiner_position) + get_position_index(self.tui.examiner_position[1]) terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?') return terrain_as_list @@ -570,8 +569,9 @@ class TUI: def move_examiner(direction): start_pos = self.examiner_position - new_examine_pos = self.game.world.map_.move(start_pos, direction) - if new_examine_pos: + new_examine_pos = self.game.map_geometry.move(start_pos, direction, + self.game.world.map_.size) + if new_examine_pos[0] == (0,0): self.examiner_position = new_examine_pos self.to_update['map'] = True diff --git a/new/plomrogue/commands.py b/new/plomrogue/commands.py index bd740b7..bcab2d1 100644 --- a/new/plomrogue/commands.py +++ b/new/plomrogue/commands.py @@ -15,7 +15,7 @@ def cmd_SEED(game, seed): cmd_SEED.argtypes = 'int:nonneg' def cmd_MAP_SIZE(game, size): - game.world.map_size = size + game.map_size = size cmd_MAP_SIZE.argtypes = 'yx_tuple:pos' def cmd_MAP(game, map_pos): @@ -104,7 +104,7 @@ def cmd_SAVE(game): with open(save_file_name, 'w') as f: write(f, 'TURN %s' % game.world.turn) write(f, 'SEED %s' % game.world.rand.prngod_seed) - write(f, 'MAP_SIZE %s' % (game.world.map_size,)) + write(f, 'MAP_SIZE %s' % (game.map_size,)) for map_pos in game.world.maps: write(f, 'MAP %s' % (map_pos,)) for map_pos in game.world.maps: diff --git a/new/plomrogue/game.py b/new/plomrogue/game.py index 669fae3..db71f21 100755 --- a/new/plomrogue/game.py +++ b/new/plomrogue/game.py @@ -8,7 +8,7 @@ from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE, cmd_GET_PICKABLE_ITEMS, cmd_MAP_SIZE, cmd_TERRAIN_LINE, cmd_PLAYER_ID, cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE) -from plomrogue.mapping import MapHex, YX +from plomrogue.mapping import MapGeometryHex, Map, YX from plomrogue.parser import Parser from plomrogue.io import GameIO from plomrogue.misc import quote @@ -67,7 +67,6 @@ class World(WorldBase): self.player_id = 0 self.player_is_alive = True self.maps = {} - self.map_size = YX(1,1) self.rand = PRNGod(0) @property @@ -81,11 +80,11 @@ class World(WorldBase): def get_map(self, map_pos, create_unfound=True): if not (map_pos in self.maps and - self.maps[map_pos].size == self.map_size): + self.maps[map_pos].size == self.game.map_size): if create_unfound: - self.maps[map_pos] = self.game.map_type(self.map_size) + self.maps[map_pos] = Map(self.game.map_size) for pos in self.maps[map_pos]: - self.maps[map_pos][pos] = '~' + self.maps[map_pos][pos] = '.' else: return None return self.maps[map_pos] @@ -143,10 +142,10 @@ class World(WorldBase): self.rand.seed(seed) self.turn = 0 self.maps = {} - self.map_size = yx + self.game.map_size = yx map_ = self.get_map(YX(0,0)) for pos in map_: - map_[pos] = self.rand.choice(('.', '.', '.', '.', 'x')) + map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x')) player = add_thing_at_random('human') self.player_id = player.id_ add_thing_at_random('monster') @@ -163,7 +162,8 @@ class Game: def __init__(self, game_file_name): self.io = GameIO(game_file_name, self) - self.map_type = MapHex + self.map_size = None + self.map_geometry = MapGeometryHex() self.tasks = {'WAIT': Task_WAIT, 'MOVE': Task_MOVE, 'PICKUP': Task_PICKUP, @@ -193,7 +193,7 @@ class Game: def get_string_options(self, string_option_type): if string_option_type == 'direction': - return self.map_type().get_directions() + return self.map_geometry.get_directions() elif string_option_type == 'thingtype': return list(self.thing_types.keys()) return None @@ -202,7 +202,9 @@ class Game: """Send out game state data relevant to clients.""" def send_thing(offset, thing): - offset_pos = (thing.position[1] - offset) + offset_pos = self.map_geometry.pos_in_projection(thing.position, + offset, + self.map_size) self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_)) self.io.send('THING_POS %s %s' % (thing.id_, offset_pos)) diff --git a/new/plomrogue/mapping.py b/new/plomrogue/mapping.py index ee3e940..bec311b 100644 --- a/new/plomrogue/mapping.py +++ b/new/plomrogue/mapping.py @@ -18,9 +18,9 @@ class YX(collections.namedtuple('YX', ('y', 'x'))): class Map: - def __init__(self, size=YX(0, 0)): + def __init__(self, size=YX(0, 0), init_char = '?'): self.size = size - self.terrain = '?'*self.size_i + self.terrain = init_char*self.size_i def __getitem__(self, yx): return self.terrain[self.get_position_index(yx)] @@ -61,8 +61,9 @@ class Map: for y in range(self.size.y): yield (y, self.terrain[y * width:(y + 1) * width]) - def get_fov_map(self, yx): - return self.fov_map_type(self, yx) + + +class MapGeometry(): def get_directions(self): directions = [] @@ -71,38 +72,45 @@ class Map: directions += [name[5:]] return directions - def get_neighbors(self, pos): + def get_neighbors(self, pos, map_size): neighbors = {} if not hasattr(self, 'neighbors_to'): self.neighbors_to = {} - if pos in self.neighbors_to: - return self.neighbors_to[pos] + if not map_size in self.neighbors_to: + self.neighbors_to[map_size] = {} + if pos in self.neighbors_to[map_size]: + return self.neighbors_to[map_size][pos] for direction in self.get_directions(): - neighbors[direction] = None - neighbor_pos = self.move(pos, direction) - if neighbor_pos: - neighbors[direction] = neighbor_pos - self.neighbors_to[pos] = neighbors + neighbors[direction] = self.move(pos, direction, map_size) + self.neighbors_to[map_size][pos] = neighbors return neighbors - def new_from_shape(self, init_char): - import copy - new_map = copy.deepcopy(self) - for pos in new_map: - new_map[pos] = init_char - return new_map + def pos_in_projection(self, pos, offset, maps_size): + pos_y = pos[1].y + (maps_size.y * pos[0].y) - offset.y + pos_x = pos[1].x + (maps_size.x * pos[0].x) - offset.x + return YX(pos_y, pos_x) + + def absolutize_coordinate(self, map_size, big_yx, little_yx): + + def adapt_axis(axis): + maps_crossed = little_yx[axis] // map_size[axis] + new_big = big_yx[axis] + maps_crossed + new_little = little_yx[axis] % map_size[axis] + return new_big, new_little - def move(self, start_pos, direction): + new_big_y, new_little_y = adapt_axis(0) + new_big_x, new_little_x = adapt_axis(1) + return YX(new_big_y, new_big_x), YX(new_little_y, new_little_x) + + def move(self, start_pos, direction, map_size): mover = getattr(self, 'move_' + direction) - new_pos = mover(start_pos) - if new_pos.y < 0 or new_pos.x < 0 or \ - new_pos.y >= self.size.y or new_pos.x >= self.size.x: - return None - return new_pos + big_yx, little_yx = start_pos + unadapted_target = mover(little_yx) + return self.absolutize_coordinate(map_size, big_yx, unadapted_target) -class MapWithLeftRightMoves(Map): +class MapGeometryWithLeftRightMoves(MapGeometry): def move_LEFT(self, start_pos): return YX(start_pos.y, start_pos.x - 1) @@ -112,7 +120,7 @@ class MapWithLeftRightMoves(Map): -class MapSquare(MapWithLeftRightMoves): +class MapGeometrySquare(MapGeometryWithLeftRightMoves): def move_UP(self, start_pos): return YX(start_pos.y - 1, start_pos.x) @@ -122,7 +130,7 @@ class MapSquare(MapWithLeftRightMoves): -class MapHex(MapWithLeftRightMoves): +class MapGeometryHex(MapGeometryWithLeftRightMoves): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -154,16 +162,16 @@ class MapHex(MapWithLeftRightMoves): -class FovMap: +class FovMap(Map): - def __init__(self, source_map, yx): + def __init__(self, source_map, center): self.source_map = source_map self.size = self.source_map.size self.fov_radius = (self.size.y / 2) - 0.5 self.terrain = '?' * self.size_i - self[yx] = '.' + self[center] = '.' self.shadow_cones = [] - self.circle_out(yx, self.shadow_process_hex) + self.circle_out(center, self.shadow_process_hex) def shadow_process_hex(self, yx, distance_to_center, dir_i, dir_progress): # Possible optimization: If no shadow_cones yet and self[yx] == '.', @@ -234,7 +242,7 @@ class FovMap: def basic_circle_out_move(self, pos, direction): """Move position pos into direction. Return whether still in map.""" - mover = getattr(self, 'move_' + direction) + mover = getattr(self.geometry, 'move_' + direction) pos = mover(pos) if pos.y < 0 or pos.x < 0 or \ pos.y >= self.size.y or pos.x >= self.size.x: @@ -271,19 +279,28 @@ class FovMap: -class FovMapHex(FovMap, MapHex): +class FovMapHex(FovMap): circle_out_directions = ('DOWNLEFT', 'LEFT', 'UPLEFT', 'UPRIGHT', 'RIGHT', 'DOWNRIGHT') + def __init__(self, *args, **kwargs): + self.geometry = MapGeometryHex() + super().__init__(*args, **kwargs) + def circle_out_move(self, yx, direction): return self.basic_circle_out_move(yx, direction) -class FovMapSquare(FovMap, MapSquare): +class FovMapSquare(FovMap): circle_out_directions = (('DOWN', 'LEFT'), ('LEFT', 'UP'), ('UP', 'RIGHT'), ('RIGHT', 'DOWN')) + def __init__(self, *args, **kwargs): + self.geometry = MapGeometrySquare() + super().__init__(*args, **kwargs) + def circle_out_move(self, yx, direction): self.basic_circle_out_move(yx, direction[0]) return self.basic_circle_out_move(yx, direction[1]) + diff --git a/new/plomrogue/tasks.py b/new/plomrogue/tasks.py index 3bc0f56..6d67c35 100644 --- a/new/plomrogue/tasks.py +++ b/new/plomrogue/tasks.py @@ -38,12 +38,13 @@ class Task_WAIT(Task): class Task_MOVE(Task): argtypes = 'string:direction' + def get_move_target(self): + return self.thing.world.game.map_geometry.move(self.thing.position, + self.args[0], + self.thing.world.game.map_size) + def check(self): - test_pos = (YX(0,0), - self.thing.world.maps[YX(0,0)]. - move(self.thing.position[1], self.args[0])) - if test_pos == (YX(0,0), None): - raise GameError('would move outside map bounds') + test_pos = self.get_move_target() if self.thing.world.maps[test_pos[0]][test_pos[1]] != '.': raise GameError('%s would move into illegal terrain' % self.thing.id_) for t in self.thing.world.things_at_pos(test_pos): @@ -51,8 +52,7 @@ class Task_MOVE(Task): raise GameError('%s would move into other thing' % self.thing.id_) def do(self): - self.thing.position = YX(0,0), self.thing.world.maps[YX(0,0)].\ - move(self.thing.position[1], self.args[0]) + self.thing.position = self.get_move_target() diff --git a/new/plomrogue/things.py b/new/plomrogue/things.py index ed3b547..c47e553 100644 --- a/new/plomrogue/things.py +++ b/new/plomrogue/things.py @@ -1,5 +1,5 @@ from plomrogue.errors import GameError -from plomrogue.mapping import YX +from plomrogue.mapping import YX, Map, FovMapHex @@ -60,18 +60,18 @@ class Thing(ThingBase): edge_up = self.position[1].y - self._radius edge_down = self.position[1].y + self._radius if edge_left < 0: - self.world.get_map(self.position[0] - YX(1,-1)) - self.world.get_map(self.position[0] - YX(0,-1)) - self.world.get_map(self.position[0] - YX(-1,-1)) - if edge_right >= self.world.map_size.x: + self.world.get_map(self.position[0] + YX(1,-1)) + self.world.get_map(self.position[0] + YX(0,-1)) + self.world.get_map(self.position[0] + YX(-1,-1)) + if edge_right >= self.world.game.map_size.x: self.world.get_map(self.position[0] + YX(1,1)) self.world.get_map(self.position[0] + YX(0,1)) self.world.get_map(self.position[0] + YX(-1,1)) if edge_up < 0: - self.world.get_map(self.position[0] - YX(-1,1)) - self.world.get_map(self.position[0] - YX(-1,0)) - self.world.get_map(self.position[0] - YX(-1,-1)) - if edge_down >= self.world.map_size.y: + self.world.get_map(self.position[0] + YX(-1,1)) + self.world.get_map(self.position[0] + YX(-1,0)) + self.world.get_map(self.position[0] + YX(-1,-1)) + if edge_down >= self.world.game.map_size.y: self.world.get_map(self.position[0] + YX(1,1)) self.world.get_map(self.position[0] + YX(1,0)) self.world.get_map(self.position[0] + YX(1,-1)) @@ -99,7 +99,7 @@ class ThingAnimate(Thing): def move_on_dijkstra_map(self, own_pos, targets): visible_map = self.get_visible_map() - dijkstra_map = self.world.game.map_type(visible_map.size) + dijkstra_map = Map(visible_map.size) n_max = 256 dijkstra_map.terrain = [n_max for i in range(dijkstra_map.size_i)] for target in targets: @@ -110,19 +110,22 @@ class ThingAnimate(Thing): for pos in dijkstra_map: if visible_map[pos] != '.': continue - neighbors = dijkstra_map.get_neighbors(pos) + neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), pos), + dijkstra_map.size) 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 + big_yx, small_yx = neighbors[direction] + if big_yx == YX(0,0) and \ + dijkstra_map[small_yx] < dijkstra_map[pos] - 1: + dijkstra_map[pos] = dijkstra_map[small_yx] + 1 shrunk = True - neighbors = dijkstra_map.get_neighbors(own_pos) + neighbors = self.world.game.map_geometry.get_neighbors((YX(0,0), own_pos), + dijkstra_map.size) n = n_max target_direction = None for direction in sorted(neighbors.keys()): - yx = neighbors[direction] - if yx is not None: - n_new = dijkstra_map[yx] + big_yx, small_yx = neighbors[direction] + if big_yx == (0,0): + n_new = dijkstra_map[small_yx] if n_new < n: n = n_new target_direction = direction @@ -241,40 +244,24 @@ class ThingAnimate(Thing): if self._surroundings_offset is not None: return self._surroundings_offset add_line = self.must_fix_indentation() - offset = YX(self.position[1].y - self._radius - int(add_line), - self.position[1].x - self._radius) + offset = YX(self.position[0].y * self.world.game.map_size.y + self.position[1].y - self._radius - int(add_line), + self.position[0].x * self.world.game.map_size.x + self.position[1].x - self._radius) self._surroundings_offset = offset return self._surroundings_offset def get_surrounding_map(self): if self._surrounding_map is not None: return self._surrounding_map - - def pan_and_scan(size_of_axis, pos, offset): - big_pos = 0 - small_pos = pos + offset - if small_pos < 0: - big_pos = -1 - small_pos = size_of_axis + small_pos - elif small_pos >= size_of_axis: - big_pos = 1 - small_pos = small_pos - size_of_axis - return big_pos, small_pos - add_line = self.must_fix_indentation() - self._surrounding_map = self.world.game.\ - map_type(size=YX(self._radius*2+1+int(add_line), - self._radius*2+1)) - size = self.world.map_size + self._surrounding_map = Map(size=YX(self._radius*2+1+int(add_line), + self._radius*2+1)) offset = self.get_surroundings_offset() for pos in self._surrounding_map: - big_y, small_y = pan_and_scan(size.y, pos.y, offset.y) - big_x, small_x = pan_and_scan(size.x, pos.x, offset.x) - big_yx = YX(big_y, big_x) - small_yx = YX(small_y, small_x) + offset_pos = pos + offset + big_yx, small_yx = self.world.game.map_geometry.absolutize_coordinate(self.world.game.map_size, (0,0), offset_pos) map_ = self.world.get_map(big_yx, False) if map_ is None: - map_ = self.world.game.map_type(size=self.world.map_size) + map_ = Map(size=self.world.game.map_size) self._surrounding_map[pos] = map_[small_yx] return self._surrounding_map @@ -282,58 +269,47 @@ class ThingAnimate(Thing): if self._stencil is not None: return self._stencil surrounding_map = self.get_surrounding_map() - m = surrounding_map.new_from_shape(' ') + m = Map(surrounding_map.size, ' ') for pos in surrounding_map: if surrounding_map[pos] in {'.', '~'}: m[pos] = '.' - offset = self.get_surroundings_offset() - fov_center = self.position[1] - offset - self._stencil = m.get_fov_map(fov_center) + add_line = self.must_fix_indentation() + fov_center = YX((add_line + m.size.y) // 2, m.size.x // 2) + self._stencil = FovMapHex(m, fov_center) return self._stencil def get_visible_map(self): stencil = self.get_stencil() - m = self.get_surrounding_map().new_from_shape(' ') + m = Map(self.get_surrounding_map().size, ' ') for pos in m: if stencil[pos] == '.': m[pos] = self._surrounding_map[pos] return m def get_visible_things(self): - - def calc_pos_in_fov(big_pos, small_pos, offset, size_of_axis): - pos = small_pos - offset - if big_pos == -1: - pos = small_pos - size_of_axis - offset - elif big_pos == 1: - pos = small_pos + size_of_axis - offset - return pos - stencil = self.get_stencil() offset = self.get_surroundings_offset() visible_things = [] - size = self.world.map_size - fov_size = self.get_surrounding_map().size for thing in self.world.things: - big_pos = thing.position[0] - small_pos = thing.position[1] - pos_y = calc_pos_in_fov(big_pos.y, small_pos.y, offset.y, size.y) - pos_x = calc_pos_in_fov(big_pos.x, small_pos.x, offset.x, size.x) - if pos_y < 0 or pos_x < 0 or\ - pos_y >= fov_size.y or pos_x >= fov_size.x: + pos = self.world.game.map_geometry.pos_in_projection(thing.position, + offset, + self.world.game.map_size) + if pos.y < 0 or pos.x < 0 or\ + pos.y >= stencil.size.y or pos.x >= stencil.size.x: continue - if (not thing.in_inventory) and stencil[YX(pos_y, pos_x)] == '.': + if (not thing.in_inventory) and stencil[pos] == '.': visible_things += [thing] return visible_things def get_pickable_items(self): pickable_ids = [] visible_things = self.get_visible_things() - for t in [t for t in visible_things if - isinstance(t, ThingItem) and + neighbor_fields = self.world.game.map_geometry.get_neighbors(self.position, + self.world.game.map_size) + for t in [t for t in visible_things + if isinstance(t, ThingItem) and (t.position == self.position or - t.position[1] in - self.world.maps[YX(0,0)].get_neighbors(self.position[1]).values())]: + t.position in neighbor_fields.values())]: pickable_ids += [t.id_] return pickable_ids -- 2.30.2