diff --git a/pyproject.toml b/pyproject.toml index c671c6b..f924a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "scrap_engine" authors = [{ name = "lxgr", email = "lxgr@protonmail.com" }] description = "A 2D ascii game engine for the terminal" keywords = ["game", "Ascii"] -version = "1.5.4" +version = "1.5.5" license = { text = "GPL-3.0-only" } readme = "README.md" requires-python = ">=3.12" @@ -21,6 +21,16 @@ Homepage = "https://github.com/lxgr-linux/scrap_engine" Issues = "https://github.com/lxgr-linux/scrap_engine/issue" Repository = "https://github.com/lxgr-linux/scrap_engine.git" +[tool.basedpyright] +reportMissingTypeStubs = false +reportUnknownMemberType = false +reportUnannotatedClassAttribute = false +deprecateTypingAliases = false +reportImplicitStringConcatenation = false +executionEnvironments = [ + { root = "src" }, +] + [tool.ruff] line-length = 88 target-version = "py312" diff --git a/src/scrap_engine/addable/addable.py b/src/scrap_engine/addable/addable.py index 5c03eba..9e3f6c6 100644 --- a/src/scrap_engine/addable/addable.py +++ b/src/scrap_engine/addable/addable.py @@ -1,7 +1,8 @@ from abc import abstractmethod from typing import Optional, Protocol -from ..map.map import Map +from scrap_engine.interfaces import IMap + from .state import DEFAULT_STATE, State @@ -17,7 +18,7 @@ class Addable(Protocol): added: bool group: Optional["Addable"] state: State - map: Optional[Map] + map: Optional[IMap] def __init__(self, state: State = DEFAULT_STATE): self.x: int = -1 @@ -28,13 +29,13 @@ def __init__(self, state: State = DEFAULT_STATE): self.added: bool = False self.group: Optional[Addable] = None self.state: State = state - self.map: Optional[Map] = None + self.map: Optional[IMap] = None def set_state(self, state: State): self.state = state @abstractmethod - def add(self, _map: Map, x: int, y: int): ... + def add(self, _map: IMap, x: int, y: int): ... @abstractmethod def remove(self): ... diff --git a/src/scrap_engine/addable/misc/box.py b/src/scrap_engine/addable/misc/box.py index fd742be..88884fc 100644 --- a/src/scrap_engine/addable/misc/box.py +++ b/src/scrap_engine/addable/misc/box.py @@ -1,7 +1,10 @@ +from typing import override + from scrap_engine.addable.addable import Addable from scrap_engine.addable.area import HasArea from scrap_engine.addable.object_group import ObjectGroup from scrap_engine.addable.state import DEFAULT_STATE +from scrap_engine.interfaces import IMap class Box(ObjectGroup[Addable], HasArea): @@ -16,14 +19,17 @@ def __init__(self, height: int, width: int): self.__width: int = width @property + @override def height(self) -> int: return self.__height @property + @override def width(self) -> int: return self.__width - def add(self, _map, x: int, y: int): + @override + def add(self, _map: IMap, x: int, y: int): """ Adds the box to a certain coordinate on a certain map. """ @@ -34,6 +40,7 @@ def add(self, _map, x: int, y: int): obj.add(self.map, obj.rx + self.x, obj.ry + self.y) self.added = True + @override def add_ob(self, obj: Addable, x: int, y: int): """ Adds an object(group) to a certain coordinate relative to the box. @@ -56,6 +63,7 @@ def set_ob(self, obj: Addable, x: int, y: int): if self.added: obj.set(obj.rx + self.x, obj.ry + self.y) + @override def remove(self): """ Removes the box from the map. diff --git a/src/scrap_engine/addable/misc/circle.py b/src/scrap_engine/addable/misc/circle.py index f84ebf5..98201e4 100644 --- a/src/scrap_engine/addable/misc/circle.py +++ b/src/scrap_engine/addable/misc/circle.py @@ -3,7 +3,7 @@ from scrap_engine.addable.state import DEFAULT_STATE, State -from ..object import Object +from ..object import ArgProto, Object from .box import Box @@ -18,7 +18,7 @@ def __init__( radius: int, state: State = DEFAULT_STATE, ob_class: Type[Object] = Object, - ob_args=None, + ob_args: ArgProto = None, ): super().__init__(0, 0) if ob_args is None: @@ -29,7 +29,7 @@ def __init__( self.state = state self.__gen(radius) - def __gen(self, radius): + def __gen(self, radius: float): self.radius = radius for i in range(-(int(radius) + 1), int(radius + 1) + 1): for j in range(-(int(radius) + 1), int(radius + 1) + 1): @@ -42,7 +42,7 @@ def __gen(self, radius): j, ) - def rechar(self, char): + def rechar(self, char: str): """ Changes the chars the circle is filled with. """ @@ -50,7 +50,7 @@ def rechar(self, char): for obj in self.obs: obj.rechar(char) - def resize(self, radius): + def resize(self, radius: float): """ Resizes the circle. """ diff --git a/src/scrap_engine/addable/misc/frame.py b/src/scrap_engine/addable/misc/frame.py index 8e9dace..a875a59 100644 --- a/src/scrap_engine/addable/misc/frame.py +++ b/src/scrap_engine/addable/misc/frame.py @@ -2,7 +2,7 @@ from scrap_engine.addable.state import DEFAULT_STATE, State -from ..object import Object +from ..object import ArgProto, Object from .box import Box from .square import Square @@ -28,7 +28,7 @@ def __init__( vertical_chars: Optional[list[str]] = None, state: State = DEFAULT_STATE, ob_class: Type[Object] = Object, - ob_args=None, + ob_args: ArgProto = None, ): super().__init__(height, width) if ob_args is None: @@ -88,9 +88,7 @@ def __gen_obs(self): for obj, rx, ry in zip(self.verticals, [0, self.width - 1], [1, 1]): self.add_ob(obj, rx, ry) - def rechar( - self, corner_chars=None, horizontal_chars=None, vertical_chars=None - ): + def rechar(self, corner_chars=None, horizontal_chars=None, vertical_chars=None): """ Rechars the frame. """ diff --git a/src/scrap_engine/addable/misc/line.py b/src/scrap_engine/addable/misc/line.py index 89891e6..8e39519 100644 --- a/src/scrap_engine/addable/misc/line.py +++ b/src/scrap_engine/addable/misc/line.py @@ -3,7 +3,7 @@ from scrap_engine.addable.state import DEFAULT_STATE, State -from ..object import Object +from ..object import ArgProto, Object from .box import Box LineType = Literal["straight", "crippled"] @@ -22,15 +22,15 @@ def __init__( l_type: LineType = "straight", state: State = DEFAULT_STATE, ob_class: Type[Object] = Object, - ob_args=None, + ob_args: ArgProto = None, ): super().__init__(0, 0) if ob_args is None: ob_args = {} - self.char = char + self.char: str = char self.ob_class = ob_class self.ob_args = ob_args - self.state = state + self.state: State = state self.type: LineType = l_type self.__gen(cx, cy) @@ -70,7 +70,7 @@ def __gen(self, cx: int, cy: int): j, ) - def rechar(self, char): + def rechar(self, char: str): """ Changes the chars the line is made from. """ diff --git a/src/scrap_engine/addable/misc/square.py b/src/scrap_engine/addable/misc/square.py index cc9ef86..3b8b4bc 100644 --- a/src/scrap_engine/addable/misc/square.py +++ b/src/scrap_engine/addable/misc/square.py @@ -2,7 +2,7 @@ from scrap_engine.addable.state import DEFAULT_STATE, State -from ..object import Object +from ..object import ArgProto, Object from .box import Box @@ -13,18 +13,17 @@ class Square(Box): def __init__( self, - char, - width, - height, + char: str, + width: int, + height: int, state: State = DEFAULT_STATE, ob_class: Type[Object] = Object, - ob_args=None, + ob_args: ArgProto = None, ): super().__init__(height, width) if ob_args is None: ob_args = {} - if state is not None: - self.state = state + self.state = state self.ob_class = ob_class self.char = char self.ob_args = ob_args @@ -34,14 +33,12 @@ def __create(self): for ry in range(self.height): for rx in range(self.width): self.add_ob( - self.ob_class( - self.char, self.state, arg_proto=self.ob_args - ), + self.ob_class(self.char, self.state, arg_proto=self.ob_args), rx, ry, ) - def rechar(self, char): + def rechar(self, char: str): """ Changes the chars the Square is filled with. """ @@ -49,7 +46,7 @@ def rechar(self, char): for obj in self.obs: obj.rechar(char) - def resize(self, width, height): + def resize(self, width: int, height: int): """ Resizes the rectangle to a certain size. """ diff --git a/src/scrap_engine/addable/misc/text.py b/src/scrap_engine/addable/misc/text.py index 9e9e25b..f1c380e 100644 --- a/src/scrap_engine/addable/misc/text.py +++ b/src/scrap_engine/addable/misc/text.py @@ -3,7 +3,7 @@ from scrap_engine.addable.area import HasArea from scrap_engine.addable.state import DEFAULT_STATE, State -from ..object import Object +from ..object import ArgProto, Object from ..object_group import ObjectGroup T = TypeVar("T", bound=Object) @@ -21,7 +21,7 @@ def __init__( state: State = DEFAULT_STATE, esccode="", ob_class: type[T] = Object, - ob_args=None, + ob_args: ArgProto = None, ignore="", ): super().__init__([], state) diff --git a/src/scrap_engine/addable/object.py b/src/scrap_engine/addable/object.py index 3b76f70..fcfb8b5 100644 --- a/src/scrap_engine/addable/object.py +++ b/src/scrap_engine/addable/object.py @@ -1,37 +1,41 @@ +from typing import Any, Optional, Self, override + +from scrap_engine.interfaces import IMap, IObject + from ..error import CoordinateError -from ..map.map import Map from .addable import Addable from .area import HasArea from .state import DEFAULT_STATE, State +type ArgProto = Optional[dict[str, Any]] # pyright: ignore[reportExplicitAny] + -class Object(HasArea, Addable): +class Object(HasArea, Addable, IObject): """ An object, containing a character, that can be added to a map. """ - def __init__(self, char: str, state: State = DEFAULT_STATE, arg_proto=None): + def __init__( + self, char: str, state: State = DEFAULT_STATE, arg_proto: ArgProto = None + ): if arg_proto is None: arg_proto = {} super().__init__(state) self.char: str = char self.arg_proto = arg_proto - self.backup = None + self.backup: str = "" - def add(self, _map: Map, x: int, y: int): + def add(self, _map: IMap, x: int, y: int): """ Adds the object to a certain coordinate on a certain map. """ if not 0 <= x < _map.width or not 0 <= y < _map.height: raise CoordinateError(self, _map, x, y) - if len(lis := _map.obmap[y][x]) != 0 and lis[-1].state == "solid": + if _map.check_collision(x, y): return 1 - self.backup = _map.map[y][x] self.x = x self.y = y - _map.map[y][x] = self.char - _map.obmap[y][x].append(self) - _map.obs.append(self) + self.backup = _map.add_object_to_pos(self, x, y) self.map = _map self.added = True return 0 @@ -40,6 +44,7 @@ def set(self, x: int, y: int): """ Sets the object to a certain coordinate. """ + assert self.map if not self.added: return 1 elif not (0 <= x < self.map.width and 0 <= y < self.map.height): @@ -48,60 +53,47 @@ def set(self, x: int, y: int): elif self.x > self.map.width - 1 or self.y > self.map.height - 1: self.pull_ob() return 1 - for obj in self.map.obmap[y][x]: - if obj.state == "solid": - self.bump(obj, x - self.x, y - self.y) - return 1 - self.__backup_setter() - self.map.obmap[y][x].append(self) - self.backup = self.map.map[y][x] + for obj in self.map.get_collision_obs(x, y, "solid"): + self.bump(obj, x - self.x, y - self.y) + return 1 + self.map.remove_ob_from_pos(self) + self.backup = self.map.set_object_to_pos(self, x, y) self.x = x self.y = y - self.map.map[y][x] = self.char - for obj in self.map.obmap[y][x]: - if obj.state == "float": - obj.action(self) + for obj in self.map.get_collision_obs(x, y, "float"): + obj.action(self) return 0 + @override def redraw(self): """ Redraws the object on the map. """ if not self.added: return 1 - self.backup = self.map.map[self.y][self.x] - self.map.map[self.y][self.x] = self.char + assert self.map + self.backup = self.map.get_char(self.x, self.y) + self.map.set_char(self.char, self.x, self.y) return 0 @property + @override def height(self) -> int: return 1 @property + @override def width(self) -> int: return 1 - def __backup_setter(self): - if ( - len(self.map.obmap[self.y][self.x]) - > self.map.obmap[self.y][self.x].index(self) + 1 - ): - self.map.obmap[self.y][self.x][ - self.map.obmap[self.y][self.x].index(self) + 1 - ].backup = self.backup - else: - self.map.map[self.y][self.x] = self.backup - del self.map.obmap[self.y][self.x][ - self.map.obmap[self.y][self.x].index(self) - ] - - def action(self, ob): + @override + def action(self, ob: Self): """ This is triggered when another object is set over this one. """ return - def bump(self, ob, x, y, side=False): + def bump(self, ob: Self, x: int, y: int, side=False): """ This is triggered, when this object is tried to be set onto another solid object. Or it hits the side of the map, in which case `side == True`. @@ -118,24 +110,30 @@ def pull_ob(self): """ return - def rechar(self, char): + def rechar(self, char: str): """ Changes the objects character. """ self.char = char if not self.added: return 1 - self.map.map[self.y][self.x] = self.backup + assert self.map + self.map.set_char(self.backup, self.x, self.y) self.redraw() return 0 + @override + def __repr__(self) -> str: + return f"Object[{self.char}]" + + @override def remove(self): """ Removes the object from the map. """ if not self.added: return 1 + assert self.map self.added = False - self.__backup_setter() - del self.map.obs[self.map.obs.index(self)] + self.map.remove_ob(self) return 0 diff --git a/src/scrap_engine/addable/object_group.py b/src/scrap_engine/addable/object_group.py index 9b1a61c..5ab9537 100644 --- a/src/scrap_engine/addable/object_group.py +++ b/src/scrap_engine/addable/object_group.py @@ -1,4 +1,4 @@ -from typing import Generic, TypeVar +from typing import Generic, TypeVar, override from .addable import Addable from .state import DEFAULT_STATE, State @@ -54,6 +54,7 @@ def move(self, x: int = 0, y: int = 0): for obj in self.obs: obj.add(self.map, obj.x + x, obj.y + y) + @override def remove(self): """ Removes all objects from their maps. @@ -61,6 +62,7 @@ def remove(self): for obj in self.obs: obj.remove() + @override def set(self, x: int, y: int): """ Sets the group to a certain coordinate. @@ -70,6 +72,7 @@ def set(self, x: int, y: int): self.x = x self.y = y + @override def set_state(self, state: State): """ Sets all objects states to a certain state. diff --git a/src/scrap_engine/interfaces/__init__.py b/src/scrap_engine/interfaces/__init__.py new file mode 100644 index 0000000..cabaefe --- /dev/null +++ b/src/scrap_engine/interfaces/__init__.py @@ -0,0 +1,56 @@ +from typing import Literal, Protocol, Self + +from scrap_engine.addable.state import State + + +class IObject(Protocol): + x: int + y: int + state: State + char: str + backup: str + + def redraw(self) -> Literal[0, 1]: ... + + def action(self, ob: Self): ... + + +class IMap(Protocol): + height: int + width: int + + def show(self, init: bool = False): ... + + def add_object_to_pos( + self, + ob: IObject, + x: int, + y: int, + ) -> str: ... + + def set_object_to_pos(self, ob: IObject, x: int, y: int) -> str: ... + + def get_collision_obs(self, x: int, y: int, state: State) -> list[IObject]: ... + + def remove_ob_from_pos(self, ob: IObject): ... + + def get_char(self, x: int, y: int) -> str: ... + + def set_char(self, char: str, x: int, y: int): ... + + def remove_ob(self, ob: IObject): ... + + def check_collision(self, x: int, y: int) -> bool: ... + + def blur_in( + self, + blurmap: Self, + esccode: str = "\033[37m", + ): ... + + def resize( + self, + height: int, + width: int, + background: str = "#", + ): ... diff --git a/src/scrap_engine/map/map.py b/src/scrap_engine/map/map.py index 518a42d..c6e492a 100644 --- a/src/scrap_engine/map/map.py +++ b/src/scrap_engine/map/map.py @@ -1,32 +1,82 @@ import functools +from typing import Callable, override + +from scrap_engine.addable.state import State +from scrap_engine.interfaces import IMap, IObject from ..consts import MAXCACHE_FRAME, MAXCACHE_LINE, screen_height, screen_width -class Map: +class Map(IMap): """ The map, objects can be added to. """ def __init__( self, - height=screen_height - 1, - width=screen_width, - background="#", - dynfps=True, + height: int = screen_height - 1, + width: int = screen_width, + background: str = "#", + dynfps: bool = True, ): - self.height = height - self.width = width - self.dynfps = dynfps - self.background = background - self.map = [ + self.height: int = height + self.width: int = width + self.dynfps: bool = dynfps + self.background: str = background + self.map: list[list[str]] = [ [self.background for _ in range(width)] for _ in range(height) ] - self.obmap = [[[] for _ in range(width)] for _ in range(height)] - self.obs = [] - self.out_old = "" + self.obmap: list[list[list[IObject]]] = [ + [[] for _ in range(width)] for _ in range(height) + ] + self.obs: list[IObject] = [] + self.out_old: str = "" + + @override + def remove_ob_from_pos(self, ob: IObject): + if len(self.obmap[ob.y][ob.x]) > self.obmap[ob.y][ob.x].index(ob) + 1: + self.obmap[ob.y][ob.x][ + self.obmap[ob.y][ob.x].index(ob) + 1 + ].backup = ob.backup + else: + self.map[ob.y][ob.x] = ob.backup + del self.obmap[ob.y][ob.x][self.obmap[ob.y][ob.x].index(ob)] + + @override + def get_char(self, x: int, y: int) -> str: + return self.map[y][x] + + @override + def set_char(self, char: str, x: int, y: int): + self.map[y][x] = char - def blur_in(self, blurmap, esccode="\033[37m"): + @override + def remove_ob(self, ob: IObject): + self.remove_ob_from_pos(ob) + del self.obs[self.obs.index(ob)] + + @override + def check_collision(self, x: int, y: int) -> bool: + return len(lis := self.obmap[y][x]) != 0 and lis[-1].state == "solid" + + @override + def add_object_to_pos(self, ob: IObject, x: int, y: int) -> str: + self.obs.append(ob) + return self.set_object_to_pos(ob, x, y) + + @override + def get_collision_obs(self, x: int, y: int, state: State) -> list[IObject]: + return [obj for obj in self.obmap[y][x] if obj.state == state] + + @override + def set_object_to_pos(self, ob: IObject, x: int, y: int) -> str: + backup = self.map[y][x] + self.map[y][x] = ob.char + self.obmap[y][x].append(ob) + return backup + + @override + def blur_in(self, blurmap: "Map", esccode: str = "\033[37m"): """ Sets another maps content as its background. """ @@ -43,7 +93,8 @@ def blur_in(self, blurmap, esccode="\033[37m"): for obj in self.obs: obj.redraw() - def show(self, init=False): + @override + def show(self, init: bool = False): """ Prints the maps content. """ @@ -55,7 +106,9 @@ def show(self, init=False): @staticmethod @functools.lru_cache(MAXCACHE_FRAME) - def __show_map(show_line, _map): + def __show_map( + show_line: Callable[[tuple[str]], str], _map: tuple[tuple[str]] + ) -> str: out = "\033[H" for arr in _map: out += show_line(arr) @@ -63,20 +116,19 @@ def __show_map(show_line, _map): @staticmethod @functools.lru_cache(MAXCACHE_LINE) - def __show_line(arr): + def __show_line(arr: tuple[str]) -> str: out_line = "" for char in arr: out_line += char return out_line - def resize(self, height, width, background="#"): + @override + def resize(self, height: int, width: int, background: str = "#"): """ Resizes the map to a certain size. """ self.background = background - self.map = [ - [self.background for _ in range(width)] for _ in range(height) - ] + self.map = [[self.background for _ in range(width)] for _ in range(height)] self.obmap = [ [[] for _ in range(max(width, self.width))] for _ in range(max(height, self.height)) diff --git a/src/tests/__init__.py b/src/tests/__init__.py index 7990c43..697672b 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -1,11 +1,12 @@ from .add_text_test import TextTest from .circle import CircleTest from .circler import CirclerTest -from .stack_test import StackTest +from .stack_test import StackTest, StackTest2 __all__ = [ "TextTest", "CircleTest", "CirclerTest", "StackTest", + "StackTest2", ] diff --git a/src/tests/stack_test.py b/src/tests/stack_test.py index dce37e1..33375f8 100644 --- a/src/tests/stack_test.py +++ b/src/tests/stack_test.py @@ -1,4 +1,5 @@ import unittest + import scrap_engine as se @@ -18,3 +19,23 @@ def test_stack(self): c.set(0, 0) map.show() + + +class StackTest2(unittest.TestCase): + def test_stack(self): + map = se.Map(background=" ", height=5, width=5) + + a = se.Object("a", state="float") + b = se.Object("b", state="float") + c = se.Object("c", state="float") + + a.add(map, 1, 1) + b.add(map, 1, 1) + c.add(map, 1, 1) + + b.set(0, 0) + c.set(0, 0) + + c.set(1, 1) + + map.show()