Browse Source

Groups of characters added.

mdo 1 year ago
parent
commit
a00fc97f38
9 changed files with 312 additions and 36 deletions
  1. 4 1
      data/02.yaml
  2. BIN
      images/characters/brick.png
  3. 1 0
      jogai/__init__.py
  4. 45 20
      jogai/character.py
  5. 163 0
      jogai/group.py
  6. 9 10
      jogai/scene.py
  7. 29 5
      jogai/videogame.py
  8. 2 0
      scripts/basics.py
  9. 59 0
      tests/test_group.py

+ 4 - 1
data/02.yaml

@@ -15,9 +15,12 @@ characters:
   jett:
   jett:
     speed: 5
     speed: 5
     width: 150
     width: 150
-    position: [400, 200]
+    position: [600, 200]
     keys:
     keys:
       forward: RIGHT
       forward: RIGHT
       backward: LEFT
       backward: LEFT
       up: UP
       up: UP
       down: DOWN
       down: DOWN
+  brick:
+    height: 40
+    position: [600, 230]

BIN
images/characters/brick.png


+ 1 - 0
jogai/__init__.py

@@ -1,6 +1,7 @@
 import pygame
 import pygame
 
 
 from jogai.character import Character
 from jogai.character import Character
+from jogai.group import *
 from jogai.scene import Scene
 from jogai.scene import Scene
 from jogai.videogame import *
 from jogai.videogame import *
 
 

+ 45 - 20
jogai/character.py

@@ -1,4 +1,5 @@
 import os
 import os
+from copy import deepcopy
 from typing import Type
 from typing import Type
 
 
 import pygame
 import pygame
@@ -43,10 +44,11 @@ class Character(pygame.sprite.Sprite):
         self.scale_factor = 1.0
         self.scale_factor = 1.0
         self.speed = 0
         self.speed = 0
         self.predefined_keys = {}
         self.predefined_keys = {}
-        self._image = None
+        self.image_filename = image_filename
+        self.image = None
         self._original_image = None
         self._original_image = None
         self._original_rect = None
         self._original_rect = None
-        self._rect = None
+        self.rect = None
         self.name = name
         self.name = name
         self.x = 0
         self.x = 0
         self.y = 0
         self.y = 0
@@ -68,6 +70,11 @@ class Character(pygame.sprite.Sprite):
         Parameters:
         Parameters:
             data: input schema
             data: input schema
         """
         """
+        if data is None:
+            data = {}
+
+        if not isinstance(data, dict):
+            raise TypeError(_('The data input must be a dict.'))
 
 
         for attribute in data.keys():
         for attribute in data.keys():
             # utils._convert_function_arguments_to_string()
             # utils._convert_function_arguments_to_string()
@@ -76,11 +83,11 @@ class Character(pygame.sprite.Sprite):
 
 
     @property
     @property
     def height(self):
     def height(self):
-        return self._rect.height
+        return self.rect.height
 
 
     @property
     @property
     def width(self):
     def width(self):
-        return self._rect.width
+        return self.rect.width
 
 
     def _eval_predefined_function(
     def _eval_predefined_function(
         self, function: str, arguments: str | list | dict
         self, function: str, arguments: str | list | dict
@@ -310,7 +317,7 @@ class Character(pygame.sprite.Sprite):
                 )
                 )
             )
             )
 
 
-        self._image = pygame.transform.scale(
+        self.image = pygame.transform.scale(
             self._original_image,
             self._original_image,
             (
             (
                 int(self._original_rect.width * scale_factor),
                 int(self._original_rect.width * scale_factor),
@@ -318,7 +325,7 @@ class Character(pygame.sprite.Sprite):
             ),
             ),
         )
         )
         self.scale_factor = scale_factor
         self.scale_factor = scale_factor
-        self._rect = self._image.get_rect()
+        self.rect = self.image.get_rect()
 
 
     def set_height(self, height: int = 0):
     def set_height(self, height: int = 0):
         """
         """
@@ -338,11 +345,11 @@ class Character(pygame.sprite.Sprite):
             raise ValueError(_('Height must be a not negative integer.'))
             raise ValueError(_('Height must be a not negative integer.'))
 
 
         self.scale_factor = height / self._original_rect.height
         self.scale_factor = height / self._original_rect.height
-        self._image = pygame.transform.scale(
+        self.image = pygame.transform.scale(
             self._original_image,
             self._original_image,
             (int(self._original_rect.width * self.scale_factor), height),
             (int(self._original_rect.width * self.scale_factor), height),
         )
         )
-        self._rect = self._image.get_rect()
+        self.rect = self.image.get_rect()
 
 
     def set_keys(self, predefined_keys: dict):
     def set_keys(self, predefined_keys: dict):
         """
         """
@@ -417,11 +424,11 @@ class Character(pygame.sprite.Sprite):
             raise ValueError(_('Width must be a not negative integer.'))
             raise ValueError(_('Width must be a not negative integer.'))
 
 
         self.scale_factor = width / self._original_rect.width
         self.scale_factor = width / self._original_rect.width
-        self._image = pygame.transform.scale(
+        self.image = pygame.transform.scale(
             self._original_image,
             self._original_image,
             (width, int(self._original_rect.height * self.scale_factor)),
             (width, int(self._original_rect.height * self.scale_factor)),
         )
         )
-        self._rect = self._image.get_rect()
+        self.rect = self.image.get_rect()
 
 
     def load(self, filename: str) -> bool:
     def load(self, filename: str) -> bool:
         """
         """
@@ -432,10 +439,25 @@ class Character(pygame.sprite.Sprite):
             settings.CHARACTERS_DIR, filename
             settings.CHARACTERS_DIR, filename
         )
         )
 
 
-        self._image = self._original_image
+        self.image = self._original_image
         self._original_rect = self._original_image.get_rect()
         self._original_rect = self._original_image.get_rect()
-        self._rect = self._image.get_rect()
+        self.rect = self.image.get_rect()
         self.scale_factor = 1
         self.scale_factor = 1
+        self.image_filename = filename
+
+    def copy(self):
+        """
+        Create a copy of this character keeping size and position.
+
+        Examples:
+            >>> ch = Character('protagonist.png')
+            >>> other = ch.copy()
+        """
+        new_character = Character(self.image_filename)
+        new_character.set_height(self.height)
+        new_character.set_width(self.width)
+        new_character.set_position((self.x, self.y))
+        return new_character
 
 
     def paint(self):
     def paint(self):
         """
         """
@@ -445,8 +467,8 @@ class Character(pygame.sprite.Sprite):
             >>> ch = Character('protagonist.png')
             >>> ch = Character('protagonist.png')
             >>> ch.paint()
             >>> ch.paint()
         """
         """
-        if self._image:
-            _window.blit(self._image, self._rect)
+        if self.image:
+            _window.blit(self.image, self.rect)
 
 
     def _update_jump(self):
     def _update_jump(self):
         """
         """
@@ -478,23 +500,26 @@ class Character(pygame.sprite.Sprite):
         if not isinstance(tracking, bool):
         if not isinstance(tracking, bool):
             raise TypeError(_('The tracking parameter must be a boolean.'))
             raise TypeError(_('The tracking parameter must be a boolean.'))
 
 
+        if not vg_scene:
+            return None
+
         if tracking:
         if tracking:
             if self.x < settings.WINDOW_WIDTH // 2:
             if self.x < settings.WINDOW_WIDTH // 2:
-                self._rect.x = self.x
+                self.rect.x = self.x
             elif (
             elif (
                 settings.WINDOW_WIDTH // 2
                 settings.WINDOW_WIDTH // 2
                 < self.x
                 < self.x
                 < vg_scene.width - settings.WINDOW_WIDTH
                 < vg_scene.width - settings.WINDOW_WIDTH
             ):
             ):
-                self._rect.x = settings.WINDOW_WIDTH // 2
+                self.rect.x = settings.WINDOW_WIDTH // 2
             elif self.x > vg_scene.width - settings.WINDOW_WIDTH:
             elif self.x > vg_scene.width - settings.WINDOW_WIDTH:
-                self._rect.x = settings.WINDOW_WIDTH * 3 / 2 - (
+                self.rect.x = settings.WINDOW_WIDTH * 3 / 2 - (
                     -self.x % vg_scene.width
                     -self.x % vg_scene.width
                 )
                 )
-            self._rect.y = self.y
+            self.rect.y = self.y
         else:
         else:
-            self._rect.x = self.x + vg_scene.x
-            self._rect.y = self.y
+            self.rect.x = self.x + vg_scene.x
+            self.rect.y = self.y
 
 
     def update(self):
     def update(self):
         for action, key in self.predefined_keys.items():
         for action, key in self.predefined_keys.items():

+ 163 - 0
jogai/group.py

@@ -0,0 +1,163 @@
+from random import randint
+from typing import Type
+
+import pygame
+
+from jogai.character import Character
+from jogai.translations import gettext as _
+
+
+class Group(pygame.sprite.Group):
+    """
+    Generic group of sprites.
+
+    Examples:
+        >>> g = Group()
+    """
+
+    def __init__(self, *args, **kwgargs):
+        super().__init__(*args, **kwgargs)
+
+
+class RandomGroup(Group):
+    """
+    Group of randomly distributed characters.
+
+    Examples:
+        >>> ch = Character('protagonist.png')
+        >>> rg = RandomGroup(ch, 5, (100, 500), (0, 300))
+    """
+
+    def __init__(
+        self,
+        model: Type['Character'],
+        amount: int,
+        xrange: tuple,
+        yrange: tuple,
+    ):
+        # TODO: check model type
+        if not isinstance(amount, int):
+            raise TypeError(
+                _('The amount of characters must be a integer greater than 1.')
+            )
+        if amount < 2:
+            raise ValueError(
+                _('The amount of characters must be a integer greater than 1.')
+            )
+
+        if isinstance(xrange, list):
+            xrange = tuple(xrange)
+        if isinstance(yrange, list):
+            yrange = tuple(yrange)
+        if not isinstance(xrange, tuple) or not isinstance(yrange, tuple):
+            raise TypeError(_('Both x and y ranges must be tuples.'))
+        if len(xrange) != 2 or len(yrange) != 2:
+            raise ValueError(
+                _('Both x and y ranges must have only two items (min, max)')
+            )
+        if xrange[0] > xrange[1] or yrange[0] > yrange[1]:
+            raise ValueError(
+                _('Both x and y ranges must be sorted (min, max)')
+            )
+
+        self.amount = amount
+        self.xrange = xrange
+        self.yrange = yrange
+
+        characters_list = []
+        for pos in range(self.amount):
+            new_character = model.copy()
+            new_character.set_position(
+                (
+                    randint(*xrange),
+                    randint(*yrange),
+                )
+            )
+            characters_list.append(new_character)
+
+        super().__init__(characters_list)
+
+
+class SquareGroup(Group):
+    """
+    Group of characters forming a square.
+
+    Examples:
+        >>> ch = Character('protagonist.png')
+        >>> sg = SquareGroup(ch, 3)
+    """
+
+    def __init__(self, model: Type['Character'], side: int):
+        # TODO: check model type
+        if not isinstance(side, int):
+            raise TypeError(_('The side of the square must be an integer.'))
+        if side < 2:
+            raise ValueError(
+                _('The side of the square must be greater than 1.')
+            )
+        self.side = side
+        self.amount = side**2
+
+        characters_list = []
+        for pos in range(self.amount):
+            new_character = model.copy()
+            new_character.set_position(
+                (
+                    model.width * (pos // self.side),
+                    model.height * (pos % self.side),
+                )
+            )
+            characters_list.append(new_character)
+
+        super().__init__(characters_list)
+
+
+class StairsGroup(Group):
+    """
+    Group of characters forming stairs.
+
+    Examples:
+        >>> ch = Character('protagonist.png')
+        >>> sg = StairsGroup(ch, 3)
+    """
+
+    def __init__(
+        self,
+        model: Type['Character'],
+        steps: int,
+        horizontal_flip: bool = False,
+        vertical_flip: bool = False,
+    ):
+        # TODO: check model type and adjust the position of elements
+        if not isinstance(steps, int):
+            raise TypeError(_('The steps of the stairs must be an integer.'))
+        if steps < 2:
+            raise ValueError(
+                _('The steps of the stairs must be greater than 1.')
+            )
+        if not isinstance(horizontal_flip, bool) or not isinstance(
+            vertical_flip, bool
+        ):
+            raise TypeError(
+                _('Both horizontal and vertical flips must be a boolean.')
+            )
+
+        self.steps = steps
+        self.amount = steps * (steps + 1) // 2
+        self.horizontal_flip = horizontal_flip
+        self.vertical_flip = vertical_flip
+
+        horizontal_sign = 1 if horizontal_flip else -1
+        vertical_sign = -1 if vertical_flip else 1
+        characters_list = [model]
+        for row in range(2, self.steps + 1):
+            for col in range(row):
+                new_character = model.copy()
+                new_character.set_position(
+                    (
+                        model.x + model.width * col * horizontal_sign,
+                        model.y + model.height * (row - 1) * vertical_sign,
+                    )
+                )
+                characters_list.append(new_character)
+        super().__init__(characters_list)

+ 9 - 10
jogai/scene.py

@@ -12,13 +12,14 @@ class Scene:
     """
     """
     A videogame scene with background color and an optional image.
     A videogame scene with background color and an optional image.
     """
     """
-    # TODO: Add background as html color
 
 
-    _image = None
-    _rect = None
+    image = None
+    rect = None
     filename = ''
     filename = ''
     background = settings.SCENE_DEFAULT_BACKGROUND
     background = settings.SCENE_DEFAULT_BACKGROUND
 
 
+    # TODO: Add background as html color
+
     def __init__(
     def __init__(
         self,
         self,
         image_filename: str = '',
         image_filename: str = '',
@@ -33,15 +34,15 @@ class Scene:
 
 
     @property
     @property
     def height(self):
     def height(self):
-        return self._rect.height
+        return self.rect.height
 
 
     @property
     @property
     def width(self):
     def width(self):
-        return self._rect.width
+        return self.rect.width
 
 
     @property
     @property
     def x(self):
     def x(self):
-        return self._rect.x
+        return self.rect.x
 
 
     def change_background(self, background: Tuple[int, int, int]):
     def change_background(self, background: Tuple[int, int, int]):
         """
         """
@@ -82,7 +83,7 @@ class Scene:
             >>> sc = Scene()
             >>> sc = Scene()
             >>> sc.change_image('test.png')
             >>> sc.change_image('test.png')
         """
         """
-        self._image, self._rect = utils._load_image(
+        self.image, self.rect = utils._load_image(
             settings.SCENES_DIR, filename
             settings.SCENES_DIR, filename
         )
         )
         self.filename = filename
         self.filename = filename
@@ -105,6 +106,4 @@ class Scene:
 
 
         # Set x in range to prevent the scene from being outside the window.
         # Set x in range to prevent the scene from being outside the window.
         # The minus sign is essential to correctly position the scene.
         # The minus sign is essential to correctly position the scene.
-        self._rect.x = -min(
-            max(0, x), self._rect.width - settings.WINDOW_WIDTH
-        )
+        self.rect.x = -min(max(0, x), self.rect.width - settings.WINDOW_WIDTH)

+ 29 - 5
jogai/videogame.py

@@ -6,7 +6,7 @@ from typing import Tuple, Type
 import pygame
 import pygame
 
 
 import jogai.settings as settings
 import jogai.settings as settings
-from jogai import character, logger, scene, utils
+from jogai import character, group, logger, scene, utils
 from jogai._events import _get_events, _get_keys
 from jogai._events import _get_events, _get_keys
 from jogai._globals import _clock, _default_data, _window
 from jogai._globals import _clock, _default_data, _window
 from jogai.translations import gettext as _
 from jogai.translations import gettext as _
@@ -16,7 +16,8 @@ from jogai.translations import gettext as _
 # --------------- #
 # --------------- #
 
 
 vg_scene = None
 vg_scene = None
-vg_characters = None
+vg_characters = []
+vg_groups = []
 running = True
 running = True
 
 
 # ------------------ #
 # ------------------ #
@@ -109,6 +110,18 @@ def add_character(new_character: 'character.Character'):
     vg_characters.append(new_character)
     vg_characters.append(new_character)
 
 
 
 
+def add_group(new_group: 'group.SquareGroup'):
+    """
+    Add a new group to the videogame.
+
+    Parameters:
+        new_group: the character to add
+    """
+    if not isinstance(new_group, group.Group):
+        raise TypeError(_('Only Group instances can be added.'))
+    vg_groups.append(new_group)
+
+
 def init():
 def init():
     """
     """
     Start the pygame engine if it is not started.
     Start the pygame engine if it is not started.
@@ -194,8 +207,12 @@ def paint(sprite: Type['scene.Scene'] | Type['character.Character']):
     # TODO: check sprite type
     # TODO: check sprite type
     if hasattr(sprite, 'background'):
     if hasattr(sprite, 'background'):
         _window.fill(sprite.background)
         _window.fill(sprite.background)
-    if sprite._image and sprite._rect:
-        _window.blit(sprite._image, sprite._rect)
+    if sprite.image and sprite.rect:
+        _window.blit(sprite.image, sprite.rect)
+
+
+def paint_group(group: Type['group.SquareGroup']):
+    group.draw(_window)
 
 
 
 
 def prepare(data_filename: str = ''):
 def prepare(data_filename: str = ''):
@@ -215,6 +232,7 @@ def prepare(data_filename: str = ''):
 
 
     vg_scene = _load_scene(vg_data)
     vg_scene = _load_scene(vg_data)
     vg_characters = _load_characters(vg_data)
     vg_characters = _load_characters(vg_data)
+
     # TODO: Check and filter variable names added to builtins
     # TODO: Check and filter variable names added to builtins
     setattr(builtins, 'vgscene', vg_scene)
     setattr(builtins, 'vgscene', vg_scene)
     for vg_character in vg_characters:
     for vg_character in vg_characters:
@@ -233,7 +251,7 @@ def update():
     """
     """
     Refresh the display surface and listen events.
     Refresh the display surface and listen events.
     """
     """
-    global _clock, _window, vg_scene, vg_characters
+    global _clock, _window, vg_scene, vg_characters, vg_groups
     if vg_scene:
     if vg_scene:
         if vg_characters:
         if vg_characters:
             vg_scene.set_position(vg_characters[0].x)
             vg_scene.set_position(vg_characters[0].x)
@@ -245,6 +263,12 @@ def update():
             )
             )
             paint(vg_character)
             paint(vg_character)
             vg_character.update()
             vg_character.update()
+    if vg_groups:
+        for g in vg_groups:
+            for e in g.sprites():
+                e._update_rect_position(vg_scene)
+                e.paint()
+
     pygame.display.flip()
     pygame.display.flip()
     _clock.tick(settings.FPS)
     _clock.tick(settings.FPS)
     listen_events()
     listen_events()

+ 2 - 0
scripts/basics.py

@@ -6,6 +6,8 @@ def test_example01():
         ...
         ...
 
 
     prepare('02.yaml')
     prepare('02.yaml')
+    sq = StairsGroup(brick, 3, False, False)
+    add_group(sq)
 
 
     while is_running():
     while is_running():
         update()
         update()

+ 59 - 0
tests/test_group.py

@@ -0,0 +1,59 @@
+import pytest
+
+from jogai import *
+
+
+def test_random_group_with_amount_wrong_type(prepare_videogame):
+    with pytest.raises(TypeError):
+        RandomGroup(protagonist, 'foo', (0, 200), (0, 200))
+
+
+def test_random_group_with_ranges_wrong_type(prepare_videogame):
+    with pytest.raises(TypeError):
+        RandomGroup(protagonist, 2, 'bar', 'foo')
+
+
+def test_random_group_with_amount_not_greater_than_1(prepare_videogame):
+    with pytest.raises(ValueError):
+        RandomGroup(protagonist, -1, (0, 200), (0, 200))
+
+
+def test_random_group_with_wrong_length_ranges(prepare_videogame):
+    with pytest.raises(ValueError):
+        RandomGroup(protagonist, 3, (1, 2, 3), (1, 2, 3))
+
+
+def test_random_group_with_min_greater_than_max_range(prepare_videogame):
+    with pytest.raises(ValueError):
+        RandomGroup(protagonist, 3, (100, 3), (100, 3))
+
+
+def test_random_group_with_lists(prepare_videogame):
+    RandomGroup(protagonist, 3, [0, 200], [0, 200])
+
+
+def test_square_group_with_wrong_side_type(prepare_videogame):
+    with pytest.raises(TypeError):
+        SquareGroup(protagonist, 'foo')
+
+
+def test_square_group_with_side_out_of_range(prepare_videogame):
+    with pytest.raises(ValueError):
+        SquareGroup(protagonist, -1)
+
+
+def test_stairs_group_with_wrong_steps_type(prepare_videogame):
+    with pytest.raises(TypeError):
+        StairsGroup(protagonist, 'foo')
+
+
+def test_stairs_group_with_steps_out_of_range(prepare_videogame):
+    with pytest.raises(ValueError):
+        StairsGroup(protagonist, -1)
+
+
+def test_stairs_group_with_horizontal_and_vertical_flip_wrong_type(
+    prepare_videogame,
+):
+    with pytest.raises(TypeError):
+        StairsGroup(protagonist, 3, 'foo', 'bar')