diff --git a/.gitignore b/.gitignore index 8499d7c..e477e04 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ env/ venv/ +local/ .coverage .pytest_cache/ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 3567ea0..91a2188 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -4,7 +4,7 @@ from enum import Enum, auto from math import sqrt from random import choice, randint -from typing import List, Optional +from typing import List, Optional, Union, Tuple from .display.texturepack import TexturePack from .translations import gettext as _ @@ -30,6 +30,28 @@ class Logs: self.messages = [] +class Slope(): + X: int + Y: int + + def __init__(self, y: int, x: int) -> None: + self.Y = y + self.X = x + + def compare(self, other: Union[Tuple[int, int], "Slope"]) -> int: + if isinstance(other, Slope): + y, x = other.Y, other.X + else: + y, x = other + return self.Y * x - self.X * y + + def __lt__(self, other: Union[Tuple[int, int], "Slope"]) -> bool: + return self.compare(other) < 0 + + def __eq__(self, other: Union[Tuple[int, int], "Slope"]) -> bool: + return self.compare(other) == 0 + + class Map: """ Object that represents a Map with its width, height @@ -40,6 +62,7 @@ class Map: start_y: int start_x: int tiles: List[List["Tile"]] + visibility: List[List[bool]] entities: List["Entity"] logs: Logs # coordinates of the point that should be @@ -54,6 +77,8 @@ class Map: self.start_y = start_y self.start_x = start_x self.tiles = tiles + self.visibility = [[False for _ in range(len(tiles[0]))] + for _ in range(len(tiles))] self.entities = [] self.logs = Logs() @@ -129,7 +154,7 @@ class Map: """ Put randomly {count} hedgehogs on the map, where it is available. """ - for ignored in range(count): + for _ignored in range(count): y, x = 0, 0 while True: y, x = randint(0, self.height - 1), randint(0, self.width - 1) @@ -140,6 +165,125 @@ class Map: entity.move(y, x) self.add_entity(entity) + def compute_visibility(self, y: int, x: int, max_range: int) -> None: + """ + Sets the visible tiles to be the ones visible by an entity at point + (y, x), using a twaked shadow casting algorithm + """ + + for line in self.visibility: + for i in range(len(line)): + line[i] = False + self.visibility[y][x] = True + for octant in range(8): + self.compute_visibility_octant(octant, (y, x), max_range, 1, + Slope(1, 1), Slope(0, 1)) + + def crop_top_visibility(self, octant: int, origin: Tuple[int, int], + x: int, top: Slope) -> int: + if top.X == 1: + top_y = x + else: + top_y = ((x * 2 - 1) * top.Y + top.X) / (top.X * 2) + if self.is_wall(top_y, x, octant, origin): + if top >= (top_y * 2 + 1, x * 2) and not\ + self.is_wall(top_y + 1, x, octant, origin): + top_y += 1 + else: + ax = x * 2 + if self.is_wall(top_y + 1, x + 1, octant, origin): + ax += 1 + if top > (top_y * 2 + 1, ax): + top_y += 1 + return top_y + + def crop_bottom_visibility(self, octant: int, origin: Tuple[int, int], + x: int, bottom: Slope) -> int: + if bottom.Y == 0: + bottom_y = 0 + else: + bottom_y = ((x * 2 + 1) * bottom.Y + bottom.X) /\ + (bottom.X * 2) + if bottom >= (bottom_y * 2 + 1, x * 2) and\ + self.is_wall(bottom_y, x, octant, origin) and\ + not self.is_wall(bottom_y + 1, x, octant, origin): + bottom_y += 1 + return bottom_y + + def compute_visibility_octant(self, octant: int, origin: Tuple[int, int], + max_range: int, distance: int, top: Slope, + bottom: Slope) -> None: + for x in range(distance, max_range): + top_y = self.crop_top_visibility(octant, origin, x, top) + bottom_y = self.crop_bottom_visibility(octant, origin, x, bottom) + was_opaque = -1 + for y in range(top_y, bottom_y - 1, -1): + if sqrt(x**2 + y**2) > max_range: + continue + is_opaque = self.is_wall(y, x, octant, origin) + is_visible = is_opaque\ + or ((y != top_y or top > (y * 4 - 1, x * 4 - 1)) + and (y != bottom_y or bottom < (y * 4 + 1, x * 4 + 1))) + if is_visible: + self.set_visible(y, x, octant, origin) + if x == max_range: + continue + if is_opaque and was_opaque == 0: + nx, ny = x * 2, y * 2 + 1 + if self.is_wall(y + 1, x, octant, origin): + nx -= 1 + if top > (ny, nx): + if y == bottom_y: + bottom = Slope(ny, nx) + break + else: + self.compute_visibility_octant( + octant, origin, max_range, x + 1, top, + Slope(ny, nx)) + else: + if y == bottom_y: + return + elif not is_opaque and was_opaque == 1: + nx, ny = x * 2, y * 2 + 1 + if self.is_wall(y + 1, x + 1, octant, origin): + nx += 1 + if bottom >= (ny, nx): + return + was_opaque = is_opaque + if was_opaque != 0: + break + + @staticmethod + def translate_coord(y: int, x: int, octant: int, + origin: Tuple[int, int]) -> Tuple[int, int]: + ny, nx = origin + if octant == 0: + return nx + x, ny - y + elif octant == 1: + return nx + y, ny - x + elif octant == 2: + return nx - y, ny - x + elif octant == 3: + return nx - x, ny - y + elif octant == 4: + return nx - x, ny + y + elif octant == 5: + return nx - y, ny + x + elif octant == 6: + return nx + y, ny + x + elif octant == 7: + return nx + x, ny + y + + def is_wall(self, y: int, x: int, octant: int, + origin: Tuple[int, int]) -> bool: + y, x = self.translate_coord(y, x, octant, origin) + return self.tiles[y][x].is_wall() + + def set_visible(self, y: int, x: int, octant: int, + origin: Tuple[int, int]) -> None: + y, x = self.translate_coord(y, x, octant, origin) + self.visibility[y][x] = True + def tick(self) -> None: """ Trigger all entity events.