From 394696ddb9dcdc131050bd1fa28862b65af85d0b Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Mon, 18 Oct 2021 17:09:44 +0300 Subject: [PATCH 01/31] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=20appdirs=20?= =?UTF-8?q?=D0=BD=D0=B0=20platformdirs=20(=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=201=20#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 2 +- requirements.txt | 2 +- spaceway/config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99abd78..cd878ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Install Space Way run: pip install . - name: Build binary - run: pyinstaller --onefile --noconsole --icon=spaceway/icon.ico --collect-all spaceway "Space Way.py" + run: pyinstaller -Fw -i spaceway/icon.ico --collect-all spaceway --hidden-import platformdirs.windows "Space Way.py" - name: Upload binary uses: actions/upload-release-asset@v1 env: diff --git a/requirements.txt b/requirements.txt index 978ae6e..ea6ed93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pygame==2.0.2.dev2 packaging==20.4 requests==2.24.0 -appdirs==1.4.4 \ No newline at end of file +platformdirs==2.1.0 \ No newline at end of file diff --git a/spaceway/config.py b/spaceway/config.py index 7995ae8..679b52f 100644 --- a/spaceway/config.py +++ b/spaceway/config.py @@ -4,7 +4,7 @@ from shutil import copyfile from json import load, dump -from appdirs import user_config_dir +from platformdirs import user_config_dir class Namespace: From 29d810b90a4388235fd0d922fb46525578f7ec6e Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Mon, 18 Oct 2021 19:32:12 +0300 Subject: [PATCH 02/31] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=86=D0=B5=D0=BD=D1=8B=20hea?= =?UTF-8?q?dpiece=20(=D1=84=D0=BE=D0=BD=20+=20=D0=BF=D1=80=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B1=D0=B0=D1=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../background.bmp => background/game.bmp} | Bin .../assets/images/background/headpiece.bmp | Bin 0 -> 1260138 bytes .../background/updater.bmp} | Bin spaceway/main.py | 12 ++-- spaceway/scenes/game/objects.py | 2 +- spaceway/scenes/headpiece/__init__.py | 5 +- spaceway/scenes/headpiece/functions.py | 13 ++-- spaceway/scenes/headpiece/objects.py | 61 ++++++++++++++---- spaceway/updater.py | 5 +- 9 files changed, 66 insertions(+), 32 deletions(-) rename spaceway/assets/images/{bg/background.bmp => background/game.bmp} (100%) create mode 100644 spaceway/assets/images/background/headpiece.bmp rename spaceway/assets/{updater/background.bmp => images/background/updater.bmp} (100%) diff --git a/spaceway/assets/images/bg/background.bmp b/spaceway/assets/images/background/game.bmp similarity index 100% rename from spaceway/assets/images/bg/background.bmp rename to spaceway/assets/images/background/game.bmp diff --git a/spaceway/assets/images/background/headpiece.bmp b/spaceway/assets/images/background/headpiece.bmp new file mode 100644 index 0000000000000000000000000000000000000000..eb78ad0f6f5d073f4045c7cbba94ed155aa57b00 GIT binary patch literal 1260138 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 KzyJen2L3;$)+vSn literal 0 HcmV?d00001 diff --git a/spaceway/assets/updater/background.bmp b/spaceway/assets/images/background/updater.bmp similarity index 100% rename from spaceway/assets/updater/background.bmp rename to spaceway/assets/images/background/updater.bmp diff --git a/spaceway/main.py b/spaceway/main.py index 2d098ba..761ef78 100644 --- a/spaceway/main.py +++ b/spaceway/main.py @@ -50,10 +50,10 @@ def main() -> None: # Define variables in namespace config['ns'].dt = 0 # Set delta-time for the further use - config['ns'].tick = 0 # Set tick for calculating the past time in seconds + config['ns'].tick = 1 # Set tick for calculating the past time in seconds # Initialization of headpiece scene - text = scenes.headpiece.init(screen, base_dir, config) + text, pb = scenes.headpiece.init(screen, base_dir, config) # Initialization of lobby scene play_button, table_button, settings_button, caption = scenes.lobby.init(screen, base_dir, config) @@ -86,13 +86,10 @@ def main() -> None: pause_lobby_button, again_button, end_lobby_button) while True: - # Update tick - config['ns'].tick += 1 - # Showing a specific scene if config['scene'] == 'headpiece': scenes.headpiece.functions.check_events(config, base_dir) - scenes.headpiece.functions.update(screen, config, text) + scenes.headpiece.functions.update(screen, config, text, pb) elif config['scene'] == 'lobby': scenes.lobby.functions.check_events(config, base_dir, scene_buttons, caption) @@ -123,3 +120,6 @@ def main() -> None: # Update screen and adjust speed to FPS pygame.display.update() config['ns'].dt = clock.tick(config['FPS']) * 0.03 + + # Update tick + config['ns'].tick += 1 diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index c4aae48..ba3ac99 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -12,7 +12,7 @@ def __init__(self, screen, base_dir, config): self.config = config - self.img = pygame.image.load(f'{base_dir}/assets/images/bg/background.bmp') + self.img = pygame.image.load(f'{base_dir}/assets/images/background/game.bmp') self.rect = FloatRect(self.img.get_rect()) def update(self): diff --git a/spaceway/scenes/headpiece/__init__.py b/spaceway/scenes/headpiece/__init__.py index b58809b..b415db0 100644 --- a/spaceway/scenes/headpiece/__init__.py +++ b/spaceway/scenes/headpiece/__init__.py @@ -2,6 +2,7 @@ def init(screen, base_dir, config): - text = Text(screen, base_dir, 'YariKartoshe4ka') + text = Text(screen, base_dir, config) + pb = ProgressBar(screen, base_dir, config) - return text + return text, pb diff --git a/spaceway/scenes/headpiece/functions.py b/spaceway/scenes/headpiece/functions.py index 583c5d4..f27f164 100644 --- a/spaceway/scenes/headpiece/functions.py +++ b/spaceway/scenes/headpiece/functions.py @@ -9,14 +9,9 @@ def check_events(config, base_dir): exit() -def update(screen, config, text): - screen.fill((0, 0, 0)) - - if config['ns'].tick % (config['FPS'] * 4) == 0: - config['scene'] = config['sub_scene'] = 'lobby' - - elif config['ns'].tick % (config['FPS'] * 2) == 0: - text.msg = 'With love' - +def update(screen, config, text, pb): text.update() text.blit() + + pb.update() + pb.blit() diff --git a/spaceway/scenes/headpiece/objects.py b/spaceway/scenes/headpiece/objects.py index 3ea328b..5e6d2fa 100644 --- a/spaceway/scenes/headpiece/objects.py +++ b/spaceway/scenes/headpiece/objects.py @@ -1,26 +1,65 @@ import pygame +from ...mixins import CaptionMixin +from ...rect import FloatRect -class Text: - def __init__(self, screen, base_dir, msg): + +class Text(CaptionMixin): + def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.msg = msg - self.color = (255, 255, 255) - self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 70) + self.img_bg = pygame.image.load(f'{base_dir}/assets/images/background/headpiece.bmp') + self.rect_bg = self.img_bg.get_rect() - self.img = self.font.render(self.msg, True, self.color) - self.rect = self.img.get_rect() + self.base_dir = base_dir - self.rect.center = self.screen_rect.center + CaptionMixin.__init__(self, base_dir, config, 'YariKartoshe4ka') def update(self): - self.img = self.font.render(self.msg, True, self.color) - self.rect = self.img.get_rect() + if self.config['ns'].tick % (self.config['FPS'] * 4) == 0: + self.config['scene'] = self.config['sub_scene'] = 'lobby' + + elif self.config['ns'].tick % (self.config['FPS'] * 2) == 0: + self.caption = 'With love' + + CaptionMixin.update(self) + def blit(self): + self.screen.blit(self.img_bg, self.rect_bg) + CaptionMixin.blit(self) + + def locate(self): self.rect.centerx = self.screen_rect.centerx - self.rect.centery = self.screen_rect.centery + self.rect.y = 50 + + +class ProgressBar: + def __init__(self, screen, base_dir, config): + self.screen = screen + self.screen_rect = self.screen.get_rect() + + self.config = config + self.color = ( + (0, 153, 255), + (252, 15, 192), + (0, 255, 0) + )[self.config['user']['color']] + + self.line = FloatRect(0, self.config['mode'][1] - 5, 0, 5) + self.inc = self.config['mode'][0] / (self.config['FPS'] * 4) + + self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 22) + + def update(self): + self.line.width += self.inc + + self.img = self.font.render(f"{round(self.line.width * 100 / self.config['mode'][0])}%", True, self.color) + self.rect = self.img.get_rect() + + self.rect.centerx = min(self.line.right, self.config['mode'][0] - self.rect.width // 2 - 5) + self.rect.bottom = self.line.top - 5 def blit(self): + pygame.draw.rect(self.screen, self.color, self.line) self.screen.blit(self.img, self.rect) diff --git a/spaceway/updater.py b/spaceway/updater.py index 613e8ff..1348961 100644 --- a/spaceway/updater.py +++ b/spaceway/updater.py @@ -1,6 +1,5 @@ """ Module responsible for the Space Way updates """ -import os from webbrowser import open import pygame @@ -23,7 +22,7 @@ def dialog(base_dir) -> None: font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 28) # Setup other drawable objects - bg = pygame.image.load(f'{base_dir}/assets/updater/background.bmp') + bg = pygame.image.load(f'{base_dir}/assets/images/background/updater.bmp') bg_rect = bg.get_rect() title_top = font.render('New version', True, (0, 255, 255)) @@ -83,7 +82,7 @@ def check_software_updates(version, base_dir) -> None: # Get remote vesrion of `config.json` if network connection available try: r = get('https://raw.githubusercontent.com/YariKartoshe4ka/Space-Way/master/spaceway/config/config.json') - except: + except Exception: return # Get value of `version` in remote version of `config.json` From db8c79c2173d7ab54ac1bdfa1c84935b3943d4e6 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Wed, 20 Oct 2021 17:30:00 +0300 Subject: [PATCH 03/31] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=80=D0=B4=D1=86=D0=B0?= =?UTF-8?q?=20=D0=BA=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D1=83=20"With=20love"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/assets/images/heart/heart.bmp | Bin 0 -> 5754 bytes spaceway/rect.py | 2 -- spaceway/scenes/headpiece/objects.py | 22 ++++++++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 spaceway/assets/images/heart/heart.bmp diff --git a/spaceway/assets/images/heart/heart.bmp b/spaceway/assets/images/heart/heart.bmp new file mode 100644 index 0000000000000000000000000000000000000000..21df4c317892fc1371d8f468e350a7c6d3f03459 GIT binary patch literal 5754 zcmcJT*>6=<6vo%No#%O;Z(G{Zx%>yb_~eWKfR8?y_~4Kj;t->WiqVKM*wPV78H^gG z2xXEPie;wEvw(mDA|f~d%Wv&{&OPhg(sGq}!|vU^Jw5&St>Ihy%zbiRA(_{Fe0>G0 z0jmznp=yf5`^^HP+BUoJt@~mk`%k{-xlcUxg7=eH-$PLs4ml&V*!hf>ghpxDSw~Al zTWQ4Ejdg&QJI826=rpYiUBbFD<09&TGP@piZLB{Zau!3|N?PK4MZ=*rSes}hv=eKB zmW2+}^3VzBJ3pf?`CFbYd5`sPKkB+cXOIS+rIwwu3hdTn3Ac(8+`RpipMQu73y)Ay(NQWXIYDJ*r>MN_3{_U1r>d%pR8w=A!r`j{R!vPe zdG_pIxuxZHB(|crtLp*HnZq$J&2qr$rU9oP`bH=_dxCOv_fcNn0V*gsL`8*@R9t+F zN=i>c-)X9-I0tPPsH*xB)r5m_Y;3$?v^6*X7Sz^xKW1I>f6&_c>0@YHgt#uItgJnT zU0$xRJ4l5EN2plX6}#-pgY0x18ybG*M#Jt_KwC%0J^DB70@|Dpsh<`?+W@qUgWb1^ z-C+|~VOQ$1(|W?;D_mQ9jqB>J^Q>7vabv>`ZbDpzT}#XF+}8F-KxZtm)!O>#eQ^88 zu*=LGx9kwxJU8zOB6)Y5%1TcMa;&EMGPHdUZP%dfM;3NE@7mh#KBl&Jsn7X<`kjT8 zk+F+JpRmh8Ty5Tgoyoi7hMneCUHu&!c6C>|zW%z=*4X$g#{tSyB^VXY&r) zT5aBSbo>Wx&THhc6K&D#auHjt4gC(1|3)-9{ceeIMi<7Ze=k!lI*$nq~Am%dY$!S0b*})t5M2bA{^=TVZGV ziQDg55Z4yuUAx4#<1g;)y3f!xZG5%19(suu(W_whCZ(ioq13c(hFwtGeuh^tYL?*@ z(04q-uCnri%{$a=IJ#yVZ*ueOTgEG-X1%=j`QN#8ueAXAL{ayHb;P!J387a;@|`w`Q$ZxOo?0caOW~JTS41j<44C z2|Y{QBFK*TD|4LD?-)L7*&*+W47+0tuW)Nt*hSZ@c!kug@3Vf*f?cQ0 zyP33gJI_!LfgQaV(1yBzww)1nm{Ax$3w8&mvMZB*A~lPCf_@hcN7t;(C~n@lHQU+Y zv(xd?-1O%O3F|p2aU&-uf5R!MTRAO#J7;7-pTrgHa-!H3p(Z?b<;c6rD#^QGziSYm zm44Uc`K;vKU6y_)c_-ZT(t7khK7Nc7pe-p0+ETVK`biYK3Ck`I>-wmn!U_4?c_73cCqE?@+Tg@4znN z6=3J(9r~TFmnwHzP6juR6%uzuOR8#gs8d578f zA$bROWzjWj^Da1}z-P_;}fXy9U+oTHSsp>~vhU9=&&Hl;d1>S{u9qdAEZz(C@NQ7lE1$ZHbPv5oqoY*IW@(vS)40euh>7;D^Rm0?~Kn*^$I(qsM%PvL(R^t&1Yw7HeqVb zO5RCqbF$$TuFvXw+WF4btYwE8%6NtDcV6Df{OQ-MzE5yJ3f;1xPihu*A^lFjA~kD# z*6(-vKAu;|Y%Fs-&a?JhA+x*8cc$NYK8yKIvD5bn?pGOIqR-@ANcvrLU0AQkh~yo- z0(qzJQ(TPM*qkdc8&|5?*yNo*qxfes-z)Te7Shu=o!X^8x9n`q8lN>@vDx~p>lONX zdnS`-6_bIPp#rnJ@e0fVawe16*k@<+PTxm+y{Ajw^);f6P_x!6#yBxSe0G!d*;LC; zUvJN|cou;3to}TBCWFsP-qp(dWY1(W2k7f#*5>7%%mCuE?u_E~JAF^g#^yYmkLM`* zb8{v$?4o8A?^yumJAM6h+NNiyd!WDXcb^9g;W^omvkcG4zQA+5)wI;vh=u25p*;be zT7%weJ$kS8=)L~k>GtwGU;xhp2Ay7L8-}(|vA)DJ&@nv2+iadOPWQX|bFD}3wI03K gzdPN&kLE+$0<0eB6L!m??Q_F!*x3Mf+a8PCKP(c#LI3~& literal 0 HcmV?d00001 diff --git a/spaceway/rect.py b/spaceway/rect.py index 55e41df..8180795 100644 --- a/spaceway/rect.py +++ b/spaceway/rect.py @@ -1,7 +1,5 @@ """ File with extension of default `pygame.Rect` to use it with float values """ -import pygame - class FloatRect: def __init__(self, *args): diff --git a/spaceway/scenes/headpiece/objects.py b/spaceway/scenes/headpiece/objects.py index 5e6d2fa..ddc5367 100644 --- a/spaceway/scenes/headpiece/objects.py +++ b/spaceway/scenes/headpiece/objects.py @@ -12,6 +12,10 @@ def __init__(self, screen, base_dir, config): self.img_bg = pygame.image.load(f'{base_dir}/assets/images/background/headpiece.bmp') self.rect_bg = self.img_bg.get_rect() + self.img_heart = pygame.image.load(f'{base_dir}/assets/images/heart/heart.bmp') + self.rect_heart = self.img_heart.get_rect() + self.is_heart = False + self.base_dir = base_dir CaptionMixin.__init__(self, base_dir, config, 'YariKartoshe4ka') @@ -22,16 +26,27 @@ def update(self): elif self.config['ns'].tick % (self.config['FPS'] * 2) == 0: self.caption = 'With love' + self.is_heart = True CaptionMixin.update(self) def blit(self): self.screen.blit(self.img_bg, self.rect_bg) + + if self.is_heart: + self.screen.blit(self.img_heart, self.rect_heart) + CaptionMixin.blit(self) def locate(self): - self.rect.centerx = self.screen_rect.centerx self.rect.y = 50 + self.rect.centerx = self.screen_rect.centerx + + if self.is_heart: + self.rect_heart.centery = self.rect.centery + + self.rect.centerx -= self.rect_heart.width - 5 + self.rect_heart.left = self.rect.right + 5 class ProgressBar: @@ -57,7 +72,10 @@ def update(self): self.img = self.font.render(f"{round(self.line.width * 100 / self.config['mode'][0])}%", True, self.color) self.rect = self.img.get_rect() - self.rect.centerx = min(self.line.right, self.config['mode'][0] - self.rect.width // 2 - 5) + self.rect.centerx = max( + self.line.left + self.rect.width // 2 + 5, + min(self.line.right, self.config['mode'][0] - self.rect.width // 2 - 5) + ) self.rect.bottom = self.line.top - 5 def blit(self): From d9e8d150915e21c1c8200bd81d134b20b760d445 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 22 Oct 2021 16:52:09 +0300 Subject: [PATCH 04/31] =?UTF-8?q?=D0=9E=D1=82=D1=80=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B0=20=D0=BE=D0=B2=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=85=D0=B8=D1=82=D0=B1=D0=BE=D0=BA=D1=81=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=20debug,=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=8B=20=D0=BD=D0=B0=D0=B4=20#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/config/config.json | 2 +- spaceway/debug.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spaceway/config/config.json b/spaceway/config/config.json index 8978d5b..426a03a 100644 --- a/spaceway/config/config.json +++ b/spaceway/config/config.json @@ -5,5 +5,5 @@ "scene": "headpiece", "sub_scene": "headpiece", "version": "2.1.0", - "debug": false + "debug": true } \ No newline at end of file diff --git a/spaceway/debug.py b/spaceway/debug.py index 5a4c96d..9eed9e6 100644 --- a/spaceway/debug.py +++ b/spaceway/debug.py @@ -95,7 +95,8 @@ class DebugHitbox(DebugModule): """ Debug module for drawing hitbox of every image """ # Color of hitbox - COLOR = (0, 255, 0, 255) + COLOR_RECT = (0, 255, 0) + COLOR_ELLIPSE = (0, 255, 255) def __init__(self) -> None: """ Initializes the module. Replaces the default image @@ -117,7 +118,8 @@ def __load_image_with_hitbox(*args, **kwargs) -> pygame.Surface: image_surface_rect = image_surface.get_rect() # Drawing hitbox on this image - pygame.draw.rect(image_surface, (0, 255, 0, 255), image_surface_rect, 1) + pygame.draw.rect(image_surface, DebugHitbox.COLOR_RECT, image_surface_rect, 1) + pygame.draw.ellipse(image_surface, DebugHitbox.COLOR_ELLIPSE, image_surface_rect, 1) return image_surface From d4b8525e5133dbf101373b5c4019be403d3ff97b Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 22 Oct 2021 16:58:46 +0300 Subject: [PATCH 05/31] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=20rect.py=20=D0=B2=20hitbox.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/{rect.py => hitbox.py} | 4 ++-- spaceway/mixins.py | 4 ++-- spaceway/scenes/game/objects.py | 12 ++++++------ spaceway/scenes/headpiece/objects.py | 4 ++-- spaceway/scenes/lobby/objects.py | 8 ++++---- spaceway/scenes/settings/objects.py | 4 ++-- spaceway/scenes/table/objects.py | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) rename spaceway/{rect.py => hitbox.py} (99%) diff --git a/spaceway/rect.py b/spaceway/hitbox.py similarity index 99% rename from spaceway/rect.py rename to spaceway/hitbox.py index 8180795..890c697 100644 --- a/spaceway/rect.py +++ b/spaceway/hitbox.py @@ -1,7 +1,7 @@ """ File with extension of default `pygame.Rect` to use it with float values """ -class FloatRect: +class Rect: def __init__(self, *args): if len(args) == 2: if len(args[0]) == 2 and len(args[1]) == 2: @@ -359,7 +359,7 @@ def collidepoint(self, *args): else: raise TypeError("argument must contain two numbers") - # conforms with no collision on right / bottom edge behavior of pygame FloatRects + # conforms with no collision on right / bottom edge behavior of pygame Rects if self._rect[0] <= point[0] < self.right: if self._rect[1] <= point[1] < self.bottom: return True diff --git a/spaceway/mixins.py b/spaceway/mixins.py index 5d59d3b..c1f46d1 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -7,7 +7,7 @@ import pygame from .collection import SceneButtonsGroup -from .rect import FloatRect +from .hitbox import Rect class SceneButtonMixin(pygame.sprite.Sprite): @@ -247,7 +247,7 @@ def __init__(self, screen, base_dir, config, name: str, life: int) -> None: self.tick = 0 # Generating a rectangle of `img_idle` and randomly positioning it - self.rect_idle = FloatRect(self.img_idle.get_rect()) + self.rect_idle = Rect(self.img_idle.get_rect()) self.rect_idle.y = randint(self.screen_rect.top, self.screen_rect.bottom - self.rect_idle.height - 2) self.rect_idle.left = self.screen_rect.right diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index ba3ac99..518b3b1 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -3,7 +3,7 @@ import pygame from ...mixins import BoostMixin, CaptionMixin, SceneButtonMixin -from ...rect import FloatRect +from ...hitbox import Rect class Background: @@ -13,7 +13,7 @@ def __init__(self, screen, base_dir, config): self.config = config self.img = pygame.image.load(f'{base_dir}/assets/images/background/game.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) def update(self): self.rect.x -= 0.5 * self.config['ns'].dt @@ -47,7 +47,7 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[self.config['user']['color']] self.img_flip = pygame.transform.flip(self.img, False, True) - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.x = 5 self.rect.centery = self.screen_rect.centery @@ -129,7 +129,7 @@ def __init__(self, screen, base_dir, config): self.img_idle = pygame.image.load(f'{base_dir}/assets/images/asteroid/gray_idle.bmp') self.img = self.img_idle - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.y = randint(1, self.screen_rect.height - self.rect.height - 2) self.rect.left = self.screen_rect.right @@ -157,7 +157,7 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[randint(0, 1)] - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.bottom = self.screen_rect.top self.rect.left = self.screen_rect.right @@ -209,7 +209,7 @@ def __init__(self, screen, base_dir, config, plate, life=5): self.img_small = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_small.bmp') self.img_active = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_activate.bmp') - self.rect_active = FloatRect(self.img_active.get_rect()) + self.rect_active = Rect(self.img_active.get_rect()) BoostMixin.__init__(self, screen, base_dir, config, 'shield', life) diff --git a/spaceway/scenes/headpiece/objects.py b/spaceway/scenes/headpiece/objects.py index ddc5367..d80ad56 100644 --- a/spaceway/scenes/headpiece/objects.py +++ b/spaceway/scenes/headpiece/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import CaptionMixin -from ...rect import FloatRect +from ...hitbox import Rect class Text(CaptionMixin): @@ -61,7 +61,7 @@ def __init__(self, screen, base_dir, config): (0, 255, 0) )[self.config['user']['color']] - self.line = FloatRect(0, self.config['mode'][1] - 5, 0, 5) + self.line = Rect(0, self.config['mode'][1] - 5, 0, 5) self.inc = self.config['mode'][0] / (self.config['FPS'] * 4) self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 22) diff --git a/spaceway/scenes/lobby/objects.py b/spaceway/scenes/lobby/objects.py index 959236a..05e7c1e 100644 --- a/spaceway/scenes/lobby/objects.py +++ b/spaceway/scenes/lobby/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import CaptionMixin, SceneButtonMixin -from ...rect import FloatRect +from ...hitbox import Rect class PlayButton(SceneButtonMixin): @@ -12,7 +12,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 90 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/play.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.top @@ -36,7 +36,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/table.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom @@ -60,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/settings.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.right = self.screen_rect.right - 5 self.rect.top = self.screen_rect.bottom diff --git a/spaceway/scenes/settings/objects.py b/spaceway/scenes/settings/objects.py index 25b58e9..c7e4dd6 100644 --- a/spaceway/scenes/settings/objects.py +++ b/spaceway/scenes/settings/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import SettingsButtonMixin, SceneButtonMixin -from ...rect import FloatRect +from ...hitbox import Rect class EffectsButton(SettingsButtonMixin): @@ -63,7 +63,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 diff --git a/spaceway/scenes/table/objects.py b/spaceway/scenes/table/objects.py index 9a13521..d908b7a 100644 --- a/spaceway/scenes/table/objects.py +++ b/spaceway/scenes/table/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import SceneButtonMixin -from ...rect import FloatRect +from ...hitbox import Rect class TableScore: @@ -60,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = FloatRect(self.img.get_rect()) + self.rect = Rect(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 From 45403b2e4d2d11d48aba63a58baddda67d52a6b9 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Wed, 27 Oct 2021 19:26:15 +0300 Subject: [PATCH 06/31] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D1=80=D0=B5=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D1=85=D0=B8=D1=82?= =?UTF-8?q?=D0=B1=D0=BE=D0=BA=D1=81=D0=BE=D0=B2=20=D1=81=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20=D0=9E=D0=9E=D0=9F=20(=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=202=20#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/hitbox.py | 452 +++++++++++++++++++++++++++++++-------------- 1 file changed, 311 insertions(+), 141 deletions(-) diff --git a/spaceway/hitbox.py b/spaceway/hitbox.py index 890c697..5b17fdc 100644 --- a/spaceway/hitbox.py +++ b/spaceway/hitbox.py @@ -1,13 +1,15 @@ """ File with extension of default `pygame.Rect` to use it with float values """ +from math import sqrt, atan2, pi, sin, cos -class Rect: + +class Hitbox: def __init__(self, *args): if len(args) == 2: if len(args[0]) == 2 and len(args[1]) == 2: l = [*args[0], *args[1]] else: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") elif len(args) == 4: l = [*args] elif len(args) == 1: @@ -21,7 +23,7 @@ def __init__(self, *args): ) else: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") self.__dict__["_rect"] = l @@ -55,7 +57,7 @@ def __getattr__(self, name): return self.__class__.getattr_dict[name](self) except KeyError: raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute 'name'" + f"'{self.__class__.__name__}' object has no attribute '{name}'" ) def __setattr__(self, name, value): @@ -159,7 +161,7 @@ def __len__(self): return 4 def __str__(self): - return f"" + return f"" def __repr__(self): return self.__str__() @@ -200,136 +202,93 @@ def inflate_ip(self, x, y): def update(self, *args): self.__init__(*args) - def clamp(self, argrect): + def clamp(self, arg): c = self.copy() - c.clamp_ip(argrect) + c.clamp_ip(arg) return c - def clamp_ip(self, argrect): + def clamp_ip(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") - - if self._rect[2] >= argrect.w: - x = argrect.x + argrect.w / 2 - self._rect[2] / 2 - elif self._rect[0] < argrect.x: - x = argrect.x - elif self._rect[0] + self._rect[2] > argrect.x + argrect.w: - x = argrect.x + argrect.w - self._rect[2] - else: - x = self._rect[0] + raise TypeError("Argument must be hitbox style object") - if self._rect[3] >= argrect.h: - y = argrect.y + argrect.h / 2 - self._rect[3] / 2 - elif self._rect[1] < argrect.y: - y = argrect.y - elif self._rect[1] + self._rect[3] > argrect.y + argrect.h: - y = argrect.y + argrect.h - self._rect[3] - else: - y = self._rect[1] + if isinstance(arg, Ellipse): + return self._clamp_ip_ellipse(Ellipse(arg)) + return self._clamp_ip_rect(Rect(arg)) - self._rect[0] = x - self._rect[1] = y + def _clamp_ip_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') + + def _clamp_ip_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') - def clip(self, argrect): + def clip(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - # left - if self.x >= argrect.x and self.x < argrect.x + argrect.w: - x = self.x - elif argrect.x >= self.x and argrect.x < self.x + self.w: - x = argrect.x - else: - return self.__class__(self.x, self.y, 0, 0) + if isinstance(arg, Ellipse): + return self.__clip_ellipse(Ellpse(arg)) + return self._clip_rect(Rect(arg)) - # right - if self.x + self.w > argrect.x and self.x + self.w <= argrect.x + argrect.w: - w = self.x + self.w - x - elif ( - argrect.x + argrect.w > self.x and argrect.x + argrect.w <= self.x + self.w - ): - w = argrect.x + argrect.w - x - else: - return self.__class__(self.x, self.y, 0, 0) + def _clip_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') - # top - if self.y >= argrect.y and self.y < argrect.y + argrect.h: - y = self.y - elif argrect.y >= self.y and argrect.y < self.y + self.h: - y = argrect.y - else: - return self.__class__(self.x, self.y, 0, 0) - - # bottom - if self.y + self.h > argrect.y and self.y + self.h <= argrect.y + argrect.h: - h = self.y + self.h - y - elif ( - argrect.y + argrect.h > self.y and argrect.y + argrect.h <= self.y + self.h - ): - h = argrect.y + argrect.h - y - else: - return self.__class__(self.x, self.y, 0, 0) + def _clip_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') - return self.__class__(x, y, w, h) - - def union(self, argrect): + def union(self, arg): c = self.copy() - c.union_ip(argrect) + c.union_ip(arg) return c - def union_ip(self, argrect): + def union_ip(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - x = min(self.x, argrect.x) - y = min(self.y, argrect.y) - w = max(self.x + self.w, argrect.x + argrect.w) - x - h = max(self.y + self.h, argrect.y + argrect.h) - y + if isinstance(arg, Ellipse): + return self._union_ip_ellipse(Ellipse(arg)) + return self._union_ip_rect(arg) - self._rect = [x, y, w, h] + def _union_ip_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') + + def _union_ip_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') - def unionall(self, argrects): + def unionall(self, args): c = self.copy() - c.unionall_ip(argrects) + c.unionall_ip(args) return c - def unionall_ip(self, argrects): - for i, argrect in enumerate(argrects): + def unionall_ip(self, args): + for arg in args: try: - argrects[i] = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - x = min([self.x] + [r.x for r in argrects]) - y = min([self.y] + [r.y for r in argrects]) - w = max([self.right] + [r.right for r in argrects]) - x - h = max([self.bottom] + [r.bottom for r in argrects]) - y - - self._rect = [x, y, w, h] + self.union_ip(arg) - def fit(self, argrect): + def fit(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - xratio = self.w / argrect.w - yratio = self.h / argrect.h - maxratio = max(xratio, yratio) - - w = self.w / maxratio - h = self.h / maxratio + if isinstance(arg, Ellipse): + return self._fit_ellipse(Ellipse(arg)) + return self._fit_rect(Rect(arg)) - x = argrect.x + (argrect.w - w) / 2 - y = argrect.y + (argrect.h - h) / 2 + def _fit_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') - return self.__class__(x, y, w, h) + def _fit_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') def normalize(self): if self._rect[2] < 0: @@ -340,16 +299,21 @@ def normalize(self): self._rect[1] += self._rect[3] self._rect[3] = -self._rect[3] - def contains(self, argrect): + def contains(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - if self._rect[0] <= argrect[0] and argrect[0] + argrect[2] <= self.right: - if self._rect[1] <= argrect[1] and argrect[1] + argrect[3] <= self.bottom: - return True - return False + if isinstance(arg, Ellipse): + return self._contains_ellipse(Ellipse(arg)) + return self._contains_rect(Rect(arg)) + + def _contains_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') + + def _contains_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') def collidepoint(self, *args): if len(args) == 1: @@ -359,66 +323,272 @@ def collidepoint(self, *args): else: raise TypeError("argument must contain two numbers") - # conforms with no collision on right / bottom edge behavior of pygame Rects - if self._rect[0] <= point[0] < self.right: - if self._rect[1] <= point[1] < self.bottom: - return True - return False + return self._collidepoint(point) - def colliderect(self, argrect): + def _collidepoint(self, point): + raise NotImplementedError('Method hasn\'t been implemented yet') + + def colliderect(self, arg): try: - argrect = self.__class__(argrect) + self.__class__(arg) except: - raise TypeError("Argument must be rect style object") + raise TypeError("Argument must be hitbox style object") - if any(0 == d for d in [self.w, self.h, argrect.w, argrect.h]): + if 0 in [self.w, self.h, arg.w, arg.h]: return False - return ( - min(self.x, self.x + self.w) < max(argrect.x, argrect.x + argrect.w) - and min(self.y, self.y + self.h) < max(argrect.y, argrect.y + argrect.h) - and max(self.x, self.x + self.w) > min(argrect.x, argrect.x + argrect.w) - and max(self.y, self.y + self.h) > min(argrect.y, argrect.y + argrect.h) - ) + if isinstance(arg, Ellipse): + return self._colliderect_ellipse(Ellipse(arg)) + return self._colliderect_rect(Rect(arg)) + + def _colliderect_rect(self, rect): + raise NotImplementedError('Method hasn\'t been implemented yet') + + def _colliderect_ellipse(self, ellipse): + raise NotImplementedError('Method hasn\'t been implemented yet') - def collidelist(self, argrects): - for i, argrect in enumerate(argrects): - if self.colliderect(argrect): + def collidelist(self, args): + for i, arg in enumerate(args): + if self.collide(arg): return i return -1 - def collidelistall(self, argrects): + def collidelistall(self, args): out = [] - for i, argrect in enumerate(argrects): - if self.colliderect(argrect): + for i, arg in enumerate(args): + if self.collide(arg): out.append(i) return out - def collidedict(self, rects_dict, use_values=0): - for key in rects_dict: + def collidedict(self, args_dict, use_values=0): + for key in args_dict: if use_values == 0: - argrect = key + arg = key else: - argrect = rects_dict[key] + arg = args_dict[key] - if self.colliderect(argrect): - return (key, rects_dict[key]) + if self.collide(arg): + return (key, args_dict[key]) return None # explicit rather than implicit - def collidedictall(self, rects_dict, use_values=0): + def collidedictall(self, args_dict, use_values=0): out = [] - for key in rects_dict: + for key in args_dict: if use_values == 0: - argrect = key + arg = key else: - argrect = rects_dict[key] + arg = args_dict[key] - if self.colliderect(argrect): - out.append((key, rects_dict[key])) + if self.collide(arg): + out.append((key, args_dict[key])) return out + + +class Rect(Hitbox): + def __str__(self): + return f"" + + def _clamp_ip_rect(self, rect): + if self._rect[2] >= rect.w: + x = rect.x + rect.w / 2 - self._rect[2] / 2 + elif self._rect[0] < rect.x: + x = rect.x + elif self._rect[0] + self._rect[2] > rect.x + rect.w: + x = rect.x + rect.w - self._rect[2] + else: + x = self._rect[0] + + if self._rect[3] >= rect.h: + y = rect.y + rect.h / 2 - self._rect[3] / 2 + elif self._rect[1] < rect.y: + y = rect.y + elif self._rect[1] + self._rect[3] > rect.y + rect.h: + y = rect.y + rect.h - self._rect[3] + else: + y = self._rect[1] + + self._rect[0] = x + self._rect[1] = y + + def _clip_rect(self, rect): + # left + if self.x >= rect.x and self.x < rect.x + rect.w: + x = self.x + elif rect.x >= self.x and rect.x < self.x + self.w: + x = rect.x + else: + return self.__class__(self.x, self.y, 0, 0) + + # right + if self.x + self.w > rect.x and self.x + self.w <= rect.x + rect.w: + w = self.x + self.w - x + elif ( + rect.x + rect.w > self.x and rect.x + rect.w <= self.x + self.w + ): + w = rect.x + rect.w - x + else: + return self.__class__(self.x, self.y, 0, 0) + + # top + if self.y >= rect.y and self.y < rect.y + rect.h: + y = self.y + elif rect.y >= self.y and rect.y < self.y + self.h: + y = rect.y + else: + return self.__class__(self.x, self.y, 0, 0) + + # bottom + if self.y + self.h > rect.y and self.y + self.h <= rect.y + rect.h: + h = self.y + self.h - y + elif ( + rect.y + rect.h > self.y and rect.y + rect.h <= self.y + self.h + ): + h = rect.y + rect.h - y + else: + return self.__class__(self.x, self.y, 0, 0) + + return self.__class__(x, y, w, h) + + def _union_ip_rect(self, rect): + x = min(self.x, rect.x) + y = min(self.y, rect.y) + w = max(self.x + self.w, rect.x + rect.w) - x + h = max(self.y + self.h, rect.y + rect.h) - y + + self._rect = [x, y, w, h] + + def _fit_rect(self, rect): + xratio = self.w / rect.w + yratio = self.h / rect.h + maxratio = max(xratio, yratio) + + w = self.w / maxratio + h = self.h / maxratio + + x = rect.x + (rect.w - w) / 2 + y = rect.y + (rect.h - h) / 2 + + return self.__class__(x, y, w, h) + + def _contains_rect(self, rect): + if self._rect[0] <= rect[0] and rect[0] + rect[2] <= self.right: + if self._rect[1] <= rect[1] and rect[1] + rect[3] <= self.bottom: + return True + return False + + def _contains_ellipse(self, ellipse): + return self._contains_rect(ellipse) + + def _collidepoint(self, point): + # conforms with no collision on right / bottom edge behavior of pygame Rects + if self._rect[0] <= point[0] < self.right: + if self._rect[1] <= point[1] < self.bottom: + return True + return False + + def _colliderect_rect(self, rect): + return ( + min(self.x, self.x + self.w) < max(rect.x, rect.x + rect.w) + and min(self.y, self.y + self.h) < max(rect.y, rect.y + rect.h) + and max(self.x, self.x + self.w) > min(rect.x, rect.x + rect.w) + and max(self.y, self.y + self.h) > min(rect.y, rect.y + rect.h) + ) + + def _colliderect_ellipse(self, ellipse): + def f_y(ellipse, x): + d = 1 - (x - ellipse.centerx)**2 / ellipse.a**2 + return (ellipse.centery - ellipse.b * sqrt(d), ellipse.centery + ellipse.b * sqrt(d)) if d > 0 else () + + def f_x(ellipse, y): + d = 1 - (y - ellipse.centery)**2 / ellipse.b**2 + return (ellipse.centerx - ellipse.a * sqrt(d), ellipse.centerx + ellipse.a * sqrt(d)) if d > 0 else () + + for i in f_x(ellipse, self.top) + f_x(ellipse, self.bottom): + if self.left <= i < self.right: + return True + + for i in f_y(ellipse, self.left) + f_y(ellipse, self.right): + if self.top <= i < self.bottom: + return True + + return False + + +class Ellipse(Hitbox): + def __init__(self, *args): + Hitbox.__init__(self, *args) + + self.getattr_dict.update({ + "a": lambda x: x._rect[2] / 2, + "b": lambda x: x._rect[3] / 2, + }) + + def __setattr__(self, name, value): + if name == "a": + self._rect[2] = value * 2 + return + + if name == "b": + self._rect[3] = value * 2 + return + + Hitbox.__setattr__(self, name, value) + + def __str__(self): + return f"" + + def radius(self, alpha): + return self.a * sin(alpha)**2 + self.b * cos(alpha)**2 + + def _contains_rect(self, rect): + return self.collidepoint(rect.topleft) and self.collidepoint(rect.bottomright) + + def _contains_ellipse(self, ellipse): + alpha = atan2(ellipse.centery - self.centery, ellipse.centerx - self.centerx) + beta = pi / 2 - alpha + + return ( + self.collidepoint(ellipse.left, ellipse.centery) and self.collidepoint(ellipse.centerx, ellipse.top) + and self.collidepoint(ellipse.right, ellipse.centery) and self.collidepoint(ellipse.centerx, ellipse.bottom) + and sqrt((ellipse.centerx - self.centerx)**2 + (ellipse.centery - self.centery)**2) + ellipse.radius(beta) + < self.radius(beta) + ) + + def _collidepoint(self, point): + if (point[0] - self.centerx)**2 / self.a**2 + (point[1] - self.centery)**2 / self.b**2 <= 1: + return True + return False + + def _colliderect_rect(self, rect): + def f_y(ellipse, x): + d = 1 - (x - ellipse.centerx)**2 / ellipse.a**2 + return (ellipse.centery - ellipse.b * sqrt(d), ellipse.centery + ellipse.b * sqrt(d)) if d > 0 else () + + def f_x(ellipse, y): + d = 1 - (y - ellipse.centery)**2 / ellipse.b**2 + return (ellipse.centerx - ellipse.a * sqrt(d), ellipse.centerx + ellipse.a * sqrt(d)) if d > 0 else () + + for i in f_x(self, rect.top) + f_x(self, rect.bottom): + if rect.left <= i < rect.right: + return True + + for i in f_y(self, rect.left) + f_y(self, rect.right): + if rect.top <= i < rect.bottom: + return True + + return False + + def _colliderect_ellipse(self, ellipse): + alpha = atan2(ellipse.centery - self.centery, ellipse.centerx - self.centerx) + beta = pi / 2 - alpha + + return ( + sqrt((ellipse.centerx - self.centerx)**2 + (ellipse.centery - self.centery)**2) + < self.radius(beta) + ellipse.radius(beta) + ) From e1a6ab76053f225d5c658f883524132158f0806a Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 29 Oct 2021 20:06:55 +0300 Subject: [PATCH 07/31] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=85?= =?UTF-8?q?=D0=B8=D1=82=D0=B1=D0=BE=D0=BA=D1=81=D0=BE=D0=B2=20(=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=203=20#9),=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 34 ++++++ spaceway/hitbox.py | 84 ++++++++++++--- tests/test_hitbox.py | 203 ++++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/check.yml create mode 100644 tests/test_hitbox.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..1dc74c7 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,34 @@ +on: + push: + branches: + - develop + + pull_request: + branches: + - develop + +name: Check + +jobs: + run-tests: + name: Run tests - Python ${{ matrix.py }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + py: + - "3.9" + - "3.8" + - "3.7" + - "3.6" + steps: + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.py }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Install dependencies + run: pip install . pytest + - name: Run tests + run: pytest --color=yes diff --git a/spaceway/hitbox.py b/spaceway/hitbox.py index 5b17fdc..e2050db 100644 --- a/spaceway/hitbox.py +++ b/spaceway/hitbox.py @@ -178,6 +178,15 @@ def __bool__(self): def copy(self): return self.__class__(self._rect) + def trunc(self): + c = self.copy() + c.trunc_ip() + return c + + def trunc_ip(self): + for i in range(len(self._rect)): + self._rect[i] = int(self._rect[i]) + def move(self, x, y): c = self.copy() c.move_ip(x, y) @@ -349,7 +358,7 @@ def _colliderect_ellipse(self, ellipse): def collidelist(self, args): for i, arg in enumerate(args): - if self.collide(arg): + if self.colliderect(arg): return i return -1 @@ -358,7 +367,7 @@ def collidelistall(self, args): out = [] for i, arg in enumerate(args): - if self.collide(arg): + if self.colliderect(arg): out.append(i) return out @@ -370,7 +379,7 @@ def collidedict(self, args_dict, use_values=0): else: arg = args_dict[key] - if self.collide(arg): + if self.colliderect(arg): return (key, args_dict[key]) return None # explicit rather than implicit @@ -384,7 +393,7 @@ def collidedictall(self, args_dict, use_values=0): else: arg = args_dict[key] - if self.collide(arg): + if self.colliderect(arg): out.append((key, args_dict[key])) return out @@ -464,8 +473,8 @@ def _union_ip_rect(self, rect): self._rect = [x, y, w, h] def _fit_rect(self, rect): - xratio = self.w / rect.w - yratio = self.h / rect.h + xratio = (self.w / rect.w) if rect.w != 0 else float('inf') + yratio = (self.h / rect.h) if rect.h != 0 else float('inf') maxratio = max(xratio, yratio) w = self.w / maxratio @@ -517,7 +526,7 @@ def f_x(ellipse, y): if self.top <= i < self.bottom: return True - return False + return self._contains_ellipse(ellipse) class Ellipse(Hitbox): @@ -540,6 +549,18 @@ def __setattr__(self, name, value): Hitbox.__setattr__(self, name, value) + @property + def f1(self): + if self.a > self.b: + return (self.centerx - sqrt(self.a**2 - self.b**2), self.centery) + return (self.centerx, self.centery - sqrt(self.b**2 - self.a**2)) + + @property + def f2(self): + if self.a > self.b: + return (self.centerx + sqrt(self.a**2 - self.b**2), self.centery) + return (self.centerx, self.centery + sqrt(self.b**2 - self.a**2)) + def __str__(self): return f"" @@ -557,7 +578,7 @@ def _contains_ellipse(self, ellipse): self.collidepoint(ellipse.left, ellipse.centery) and self.collidepoint(ellipse.centerx, ellipse.top) and self.collidepoint(ellipse.right, ellipse.centery) and self.collidepoint(ellipse.centerx, ellipse.bottom) and sqrt((ellipse.centerx - self.centerx)**2 + (ellipse.centery - self.centery)**2) + ellipse.radius(beta) - < self.radius(beta) + <= self.radius(beta) ) def _collidepoint(self, point): @@ -582,13 +603,48 @@ def f_x(ellipse, y): if rect.top <= i < rect.bottom: return True - return False + return self._contains_rect(rect) def _colliderect_ellipse(self, ellipse): - alpha = atan2(ellipse.centery - self.centery, ellipse.centerx - self.centerx) + f1, f2 = self.f1, self.f2 + + alpha = atan2(ellipse.centery - f1[1], f1[0] - ellipse.centerx) + beta = pi / 2 - alpha + r1 = ellipse.radius(beta) + rx1, ry1 = (ellipse.centerx - -r1 * sin(beta), ellipse.centery + -r1 * cos(beta)) + + alpha = atan2(ellipse.centery - f2[1], ellipse.centerx - f2[0]) beta = pi / 2 - alpha + r2 = ellipse.radius(beta) + rx2, ry2 = (ellipse.centerx + -r2 * sin(beta), ellipse.centery + -r2 * cos(beta)) + + mhaxis = max(self.a, self.b) + + if ( + sqrt((rx1 - f1[0])**2 + (ry1 - f1[1])**2) + sqrt((rx1 - f2[0])**2 + (ry1 - f2[1])**2) <= 2 * mhaxis or + sqrt((rx2 - f1[0])**2 + (ry2 - f1[1])**2) + sqrt((rx2 - f2[0])**2 + (ry2 - f2[1])**2) <= 2 * mhaxis + ): + return True + + f1, f2 = ellipse.f1, ellipse.f2 + + alpha = atan2(self.centery - f1[1], f1[0] - self.centerx) + beta = pi / 2 - alpha + r1 = self.radius(beta) + rx1, ry1 = (self.centerx - -r1 * sin(beta), self.centery + -r1 * cos(beta)) + + alpha = atan2(self.centery - f2[1], self.centerx - f2[0]) + beta = pi / 2 - alpha + r2 = self.radius(beta) + rx2, ry2 = (self.centerx + -r2 * sin(beta), self.centery + -r2 * cos(beta)) + + mhaxis = max(ellipse.a, ellipse.b) + + if ( + sqrt((rx1 - f1[0])**2 + (ry1 - f1[1])**2) + sqrt((rx1 - f2[0])**2 + (ry1 - f2[1])**2) <= 2 * mhaxis or + sqrt((rx2 - f1[0])**2 + (ry2 - f1[1])**2) + sqrt((rx2 - f2[0])**2 + (ry2 - f2[1])**2) <= 2 * mhaxis + ): + return True + + return False - return ( - sqrt((ellipse.centerx - self.centerx)**2 + (ellipse.centery - self.centery)**2) - < self.radius(beta) + ellipse.radius(beta) - ) diff --git a/tests/test_hitbox.py b/tests/test_hitbox.py new file mode 100644 index 0000000..3806d03 --- /dev/null +++ b/tests/test_hitbox.py @@ -0,0 +1,203 @@ +import pytest +from random import randint + +from spaceway.hitbox import Rect, Ellipse +from pygame import Rect as PgRect + + +def test_rect_init(): + assert ( + Rect(100, 123, 10, 29) == Rect((100, 123), (10, 29)) == + Rect(Rect(100, 123, 10, 29)) == Rect(Ellipse(100, 123, 10, 29)) == + Rect(PgRect(100, 123, 10, 29)) == PgRect(Rect(100, 123, 10, 29)) + ) + assert ( + Rect(-100, 2, 0, -2) == Rect((-100, 2), (0, -2)) == + Rect(Rect(-100, 2, 0, -2)) == Rect(Ellipse(-100, 2, 0, -2)) == + Rect(PgRect(-100, 2, 0, -2)) == PgRect(Rect(-100, 2, 0, -2)) + ) + + +def test_ellipse_init(): + assert ( + Ellipse(100, 123, 10, 29) == Ellipse((100, 123), (10, 29)) == + Ellipse(Ellipse(100, 123, 10, 29)) == Ellipse(Rect(100, 123, 10, 29)) == + Ellipse(PgRect(100, 123, 10, 29)) == PgRect(Ellipse(100, 123, 10, 29)) + ) + assert ( + Ellipse(-100, 2, 0, -2) == Ellipse((-100, 2), (0, -2)) == + Ellipse(Ellipse(-100, 2, 0, -2)) == Ellipse(Rect(-100, 2, 0, -2)) == + Ellipse(PgRect(-100, 2, 0, -2)) == PgRect(Ellipse(-100, 2, 0, -2)) + ) + + +@pytest.mark.parametrize('hitbox', [ + Rect(100, 123, 10, 29), + Rect(-100, 2, 0, -2), + Ellipse(100, 123, 10, 29), + Ellipse(-100, 2, 0, -2) +]) +def test_generic(hitbox): + # Test `copy` method + assert hitbox.copy() == hitbox + + # Test magic methods + hitbox_copy = hitbox.copy() + + hitbox_copy[0], hitbox_copy[1], hitbox_copy[2], hitbox_copy[3] = range(4) + assert ( + hitbox_copy[0] == 0 and hitbox_copy[1] == 1 and + hitbox_copy[2] == 2 and hitbox_copy[3] == 3 + ) + assert len(hitbox) == len(PgRect(hitbox)) + assert bool(hitbox) == bool(PgRect(hitbox)) + + + # Test `getattr_dict` and methods which operates with it + hitbox_copy = hitbox.copy() + + for attr in hitbox_copy.getattr_dict.keys(): + a = getattr(hitbox_copy, attr) + + if isinstance(a, int) or isinstance(a, float): + v1 = randint(-1000, 1000) + v2 = 0 + else: + v1 = (randint(-1000, 1000), randint(-1000, 1000)) + v2 = (0, 0) + + # Log attribute name on fail + print(f'attr={attr} v1={v1} v2={v2}' + ' ' * 15, end='\r') + + setattr(hitbox_copy, attr, v1) + assert getattr(hitbox_copy, attr) == v1 + + setattr(hitbox_copy, attr, v2) + assert getattr(hitbox_copy, attr) == v2 + + # Test other methods + x, y = (10, -10) + + # Test `trunc` (`trunc_ip`) + hitbox_copy = hitbox.copy() + for i in range(len(hitbox_copy._rect)): + hitbox_copy._rect[i] += 1e-04 + + assert hitbox_copy.trunc() == PgRect(hitbox_copy) + + # Test `move` (`move_ip`) and `inflate` (`inflate_ip`) + assert hitbox.move(x, y).trunc() == PgRect(hitbox).move(x, y) + assert hitbox.inflate(x, y).trunc() == PgRect(hitbox).inflate(x, y) + + # Test `normalize` + hitbox_copy = hitbox.copy() + pgrect = PgRect(hitbox_copy) + hitbox_copy.normalize() + pgrect.normalize() + + assert hitbox_copy == pgrect + + # Test `update` + hitbox_copy.update(hitbox) + assert hitbox_copy == hitbox + + +@pytest.mark.parametrize('rect', [ + Rect(23, 83, 40, 10), + Rect(100, 83, 120, 251), +]) +@pytest.mark.parametrize('arg', [ + Rect(100, 123, 10, 29), + Rect(-100, 2, 0, -2), + PgRect(100, 123, 10, 29), + PgRect(-100, 2, 0, -2) +]) +def test_rect_with_rect(rect, arg): + # Test `clamp (_ip)`, `clip`, `union (_ip)`, `fit`, `contains` and `colliderect` + assert rect.clamp(arg).trunc() == PgRect(rect).clamp(arg) + assert rect.clip(arg).trunc() == PgRect(rect).clip(arg) + assert rect.union(arg).trunc() == PgRect(rect).union(arg) + assert rect.fit(arg).trunc() == PgRect(rect).fit(arg) + assert rect.contains(arg) == PgRect(rect).contains(arg) + assert rect.colliderect(arg) == PgRect(rect).colliderect(arg) + + # Test `unionall (_ip)`, `collidelist` and `collidelistall` + assert rect.unionall([arg, rect]) == PgRect(rect).unionall([arg, rect]) + assert rect.collidelist([arg, rect, arg, rect]) == PgRect(rect).collidelist([arg, rect, arg, rect]) + assert rect.collidelistall([arg, rect, arg, rect]) == PgRect(rect).collidelistall([arg, rect, arg, rect]) + + # Test `collidedict` and `collidedictall` + assert ( + rect.collidedict({0: arg, 1: rect, True: arg, None: rect}, True) == + PgRect(rect).collidedict({0: arg, 1: rect, True: arg, None: rect}, True) + ) + assert ( + rect.collidedictall({0: arg, 1: rect, True: arg, None: rect}, True) == + PgRect(rect).collidedictall({0: arg, 1: rect, True: arg, None: rect}, True) + ) + + # Test `collidepoint` + point1 = (randint(-20, 20), randint(-20, 20)) + point2 = (0, 0) + + assert rect.collidepoint(point1) == PgRect(rect).collidepoint(point1) + assert rect.collidepoint(*point1) == PgRect(rect).collidepoint(*point1) + + assert rect.collidepoint(point2) == PgRect(rect).collidepoint(point2) + assert rect.collidepoint(*point2) == PgRect(rect).collidepoint(*point2) + + +@pytest.mark.parametrize('rect,arg,expected', [ + (Rect(10, 50, 83, 127), Ellipse(68, 59, 90, 72), (False, True)), + (Rect(140, 152, 103, 73), Ellipse(61, 76, 90, 81), (False, False)), + (Rect(-30, -16, 88, 73), Ellipse(53, 30, 90, 81), (False, True)), + (Rect(71, 24, 190, 69), Ellipse(83, -65, 140, 92), (False, True)), + (Rect(71, 213, 180, 140), Ellipse(102, 226, 140, 125), (True, True)) +]) +def test_rect_with_ellipse(rect, arg, expected): + assert rect.contains(arg) == expected[0] + assert rect.colliderect(arg) == expected[1] + + +@pytest.mark.parametrize('ellipse,expected', [ + (Ellipse(150, 70, 73, 130), (False,)), + (Ellipse(198, 190, 65, 20), (True,)), + (Ellipse(196, 188, 58, 131), (False,)), + (Ellipse(150, 160, 97, 90), (True,)) +]) +def test_ellipse(ellipse, expected): + point = (200, 200) + assert ellipse.collidepoint(point) == expected[0] + assert ellipse.collidepoint(*point) == expected[0] + + +@pytest.mark.parametrize('ellipse,arg,expected', [ + (Ellipse(20, 30, 41, 70), Rect(55, 51, 61, 40), (False, True)), + (Ellipse(30, 50, 80, 42), Rect(-5, -10, 40, 68), (False, False)), + (Ellipse(40, 80, 70, 59), Rect(98, 57, 70, 40), (False, True)), + (Ellipse(-70, 39, 42, 80), Rect(-30, 39, 42, 80), (False, True)), + (Ellipse(71, 42, 73, 130), Rect(99, 52, 10, 5), (True, True)), + (Ellipse(20, 30, 41, 70), PgRect(55, 51, 61, 40), (False, True)), + (Ellipse(30, 50, 80, 42), PgRect(-5, -10, 40, 68), (False, False)), + (Ellipse(40, 80, 70, 59), PgRect(98, 57, 70, 40), (False, True)), + (Ellipse(-70, 39, 42, 80), PgRect(-30, 39, 42, 80), (False, True)), + (Ellipse(71, 42, 73, 130), PgRect(99, 52, 10, 5), (True, True)) +]) +def test_ellipse_with_rect(ellipse, arg, expected): + assert ellipse.contains(arg) == expected[0] + assert ellipse.colliderect(arg) == expected[1] + + +@pytest.mark.parametrize('ellipse,arg,expected', [ + (Ellipse(17, 28, 124, 90), Ellipse(42, 10, 91, 130), (False, True)), + (Ellipse(51, 10, 38, 70), Ellipse(85, 65, 120, 39), (False, False)), + (Ellipse(104, 201, 120, 180), Ellipse(50, 176, 84, 39), (False, False)), + (Ellipse(20, 87, 111, 98), Ellipse(86, 100, 36, 45), (True, True)), + (Ellipse(-40, 32, 85, 24), Ellipse(44, 17, 30, 54), (False, True)), + (Ellipse(58, 74, 198, 166), Ellipse(76, 91, 54, 50), (False, True)), + (Ellipse(-100, -100, 115, 84), Ellipse(-100, -100, 115, 84), (True, True)), + # (Ellipse(34, 80, 10, 100), Ellipse(34, 82, 100, 10), (False, True)) - Problem test +]) +def test_ellipse_with_ellipse(ellipse, arg, expected): + assert ellipse.contains(arg) == expected[0] + assert ellipse.colliderect(arg) == expected[1] From 810462b7c094d87d22b53119aa033e4a30db775a Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 29 Oct 2021 20:19:08 +0300 Subject: [PATCH 08/31] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=BE=D0=B1?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=B8=D0=BC=D1=8B=D1=85=20=D0=BE=D0=B1=D1=8A?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D1=81=20Rect=20=D0=BD=D0=B0?= =?UTF-8?q?=20Ellipse,=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/config/config.json | 2 +- spaceway/mixins.py | 6 +++--- spaceway/scenes/game/objects.py | 16 ++++++++-------- spaceway/scenes/lobby/objects.py | 8 ++++---- spaceway/scenes/settings/objects.py | 4 ++-- spaceway/scenes/table/objects.py | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/spaceway/config/config.json b/spaceway/config/config.json index 426a03a..8978d5b 100644 --- a/spaceway/config/config.json +++ b/spaceway/config/config.json @@ -5,5 +5,5 @@ "scene": "headpiece", "sub_scene": "headpiece", "version": "2.1.0", - "debug": true + "debug": false } \ No newline at end of file diff --git a/spaceway/mixins.py b/spaceway/mixins.py index c1f46d1..e4a43d1 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -7,7 +7,7 @@ import pygame from .collection import SceneButtonsGroup -from .hitbox import Rect +from .hitbox import Rect, Ellipse class SceneButtonMixin(pygame.sprite.Sprite): @@ -193,7 +193,7 @@ def __init__(self, screen, config, config_index: str) -> None: # Setting image by current state and getting its rectangle self.img = self.imgs[self.state] - self.rect = self.img.get_rect() + self.rect = Ellipse(self.img.get_rect()) def change_state(self) -> None: """ Changes state of button. By default it has on-off behaviour """ @@ -247,7 +247,7 @@ def __init__(self, screen, base_dir, config, name: str, life: int) -> None: self.tick = 0 # Generating a rectangle of `img_idle` and randomly positioning it - self.rect_idle = Rect(self.img_idle.get_rect()) + self.rect_idle = Ellipse(self.img_idle.get_rect()) self.rect_idle.y = randint(self.screen_rect.top, self.screen_rect.bottom - self.rect_idle.height - 2) self.rect_idle.left = self.screen_rect.right diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index 518b3b1..a2bad7f 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -3,7 +3,7 @@ import pygame from ...mixins import BoostMixin, CaptionMixin, SceneButtonMixin -from ...hitbox import Rect +from ...hitbox import Rect, Ellipse class Background: @@ -47,7 +47,7 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[self.config['user']['color']] self.img_flip = pygame.transform.flip(self.img, False, True) - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.x = 5 self.rect.centery = self.screen_rect.centery @@ -129,7 +129,7 @@ def __init__(self, screen, base_dir, config): self.img_idle = pygame.image.load(f'{base_dir}/assets/images/asteroid/gray_idle.bmp') self.img = self.img_idle - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.y = randint(1, self.screen_rect.height - self.rect.height - 2) self.rect.left = self.screen_rect.right @@ -157,7 +157,7 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[randint(0, 1)] - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.bottom = self.screen_rect.top self.rect.left = self.screen_rect.right @@ -309,7 +309,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/resume.bmp') - self.rect = self.img.get_rect() + self.rect = Ellipse(self.img.get_rect()) SceneButtonMixin.__init__(self, base_dir, config, 'game', 'pause', 'game', 'game', 0) @@ -322,7 +322,7 @@ def __init__(self, screen, base_dir, config, defeat, *defeat_args): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') - self.rect = self.img.get_rect() + self.rect = Ellipse(self.img.get_rect()) self.defeat = defeat self.defeat_args = defeat_args @@ -344,7 +344,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/again.bmp') - self.rect = self.img.get_rect() + self.rect = Ellipse(self.img.get_rect()) SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'game', 'game', 0) @@ -357,6 +357,6 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') - self.rect = self.img.get_rect() + self.rect = Ellipse(self.img.get_rect()) SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'lobby', 'lobby', 0) diff --git a/spaceway/scenes/lobby/objects.py b/spaceway/scenes/lobby/objects.py index 05e7c1e..2484b7a 100644 --- a/spaceway/scenes/lobby/objects.py +++ b/spaceway/scenes/lobby/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import CaptionMixin, SceneButtonMixin -from ...hitbox import Rect +from ...hitbox import Ellipse class PlayButton(SceneButtonMixin): @@ -12,7 +12,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 90 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/play.bmp') - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.top @@ -36,7 +36,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/table.bmp') - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom @@ -60,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/settings.bmp') - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.right = self.screen_rect.right - 5 self.rect.top = self.screen_rect.bottom diff --git a/spaceway/scenes/settings/objects.py b/spaceway/scenes/settings/objects.py index c7e4dd6..a4d5c56 100644 --- a/spaceway/scenes/settings/objects.py +++ b/spaceway/scenes/settings/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import SettingsButtonMixin, SceneButtonMixin -from ...hitbox import Rect +from ...hitbox import Ellipse class EffectsButton(SettingsButtonMixin): @@ -63,7 +63,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 diff --git a/spaceway/scenes/table/objects.py b/spaceway/scenes/table/objects.py index d908b7a..2d7ea95 100644 --- a/spaceway/scenes/table/objects.py +++ b/spaceway/scenes/table/objects.py @@ -1,7 +1,7 @@ import pygame from ...mixins import SceneButtonMixin -from ...hitbox import Rect +from ...hitbox import Ellipse class TableScore: @@ -60,7 +60,7 @@ def __init__(self, screen, base_dir, config): self.width = self.height = 63 self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') - self.rect = Rect(self.img.get_rect()) + self.rect = Ellipse(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - 5 From 3477b7524e6193ee3317648fcaa3477e73ed5b98 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 30 Oct 2021 17:51:25 +0300 Subject: [PATCH 09/31] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=82=D1=80=D0=B8=D1=81=D0=BE=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=85=D0=B8=D1=82=D0=B1=D0=BE=D0=BA=D1=81=D0=BE=D0=B2=20=D0=B2?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=20debug,=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=85=D0=B8=D1=82?= =?UTF-8?q?=D0=B1=D0=BE=D0=BA=D1=81=D0=B0=20=D0=B4=D0=BB=D1=8F=20FlyingAst?= =?UTF-8?q?eroid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/debug.py | 44 ++++++++++++++++++++++----------- spaceway/main.py | 2 +- spaceway/scenes/game/objects.py | 19 ++++++++------ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/spaceway/debug.py b/spaceway/debug.py index 9eed9e6..da83c50 100644 --- a/spaceway/debug.py +++ b/spaceway/debug.py @@ -1,8 +1,12 @@ """ File with some objects for easier debugging of game """ +from weakref import ref + import pygame from psutil import Process, cpu_count +from .hitbox import Hitbox, Ellipse + class DebugModule: """ Debug module - part of Debugger. Every debug module must have at least @@ -98,30 +102,40 @@ class DebugHitbox(DebugModule): COLOR_RECT = (0, 255, 0) COLOR_ELLIPSE = (0, 255, 255) - def __init__(self) -> None: - """ Initializes the module. Replaces the default image - loading function with an custom """ + def __init__(self, screen) -> None: + """ Initializes the module. Replaces the default `__init__` + function of `Hitbox` to track its instances """ + + # Saving screen for the further use + self.screen = screen + + # Creating list of hiboxes + self.hitboxes = [] - # Saving default function - globals()['pygame_image_load'] = pygame.image.load + # Saving original `__init__` and list of hitboxes + globals()['origin_hitbox_init'] = Hitbox.__init__ + globals()['hitboxes'] = self.hitboxes - # Replacing default function with custom function - pygame.image.load = self.__load_image_with_hitbox + # Replacing default `__init__` function with custom function + Hitbox.__init__ = self.__hitbox_init @staticmethod - def __load_image_with_hitbox(*args, **kwargs) -> pygame.Surface: + def __hitbox_init(self, *args, **kwargs): """ Loading image via default loading images function and adding to this image hitbox """ - # Loading image via default pygame function and getting rect of it - image_surface = pygame_image_load(*args, **kwargs).convert_alpha() - image_surface_rect = image_surface.get_rect() + # Adding hitbox to list + hitboxes.append(ref(self, hitboxes.remove)) - # Drawing hitbox on this image - pygame.draw.rect(image_surface, DebugHitbox.COLOR_RECT, image_surface_rect, 1) - pygame.draw.ellipse(image_surface, DebugHitbox.COLOR_ELLIPSE, image_surface_rect, 1) + # Calling original `__init__` function + return origin_hitbox_init(self, *args, **kwargs) - return image_surface + def static_update(self): + for hitbox in self.hitboxes: + if isinstance(hitbox(), Ellipse): + pygame.draw.ellipse(self.screen, self.COLOR_ELLIPSE, hitbox(), 1) + else: + pygame.draw.rect(self.screen, self.COLOR_RECT, hitbox(), 1) class Debugger: diff --git a/spaceway/main.py b/spaceway/main.py index 761ef78..f572ba2 100644 --- a/spaceway/main.py +++ b/spaceway/main.py @@ -46,7 +46,7 @@ def main() -> None: from . import debug debugger = debug.Debugger(config['FPS']) debugger.enable_module(debug.DebugStat, screen, base_dir, clock) - debugger.enable_module(debug.DebugHitbox) + debugger.enable_module(debug.DebugHitbox, screen) # Define variables in namespace config['ns'].dt = 0 # Set delta-time for the further use diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index a2bad7f..f3834ab 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -157,16 +157,21 @@ def __init__(self, screen, base_dir, config): self.img = self.imgs[randint(0, 1)] - self.rect = Ellipse(self.img.get_rect()) - self.rect.bottom = self.screen_rect.top - self.rect.left = self.screen_rect.right + self.rect_blit = Rect(self.img.get_rect()) + self.rect_blit.bottom = self.screen_rect.top + self.rect_blit.left = self.screen_rect.right + + self.rect = Ellipse(0, 0, 60, 60) + self.rect.bottomleft = self.rect_blit.bottomleft def blit(self): - self.screen.blit(self.img, self.rect) + self.screen.blit(self.img, self.rect_blit) def update(self): - self.rect.x -= self.config['ns'].speed * 1.5 * self.config['ns'].dt - self.rect.y += self.config['ns'].speed * self.config['ns'].dt + self.rect_blit.x -= self.config['ns'].speed * 1.5 * self.config['ns'].dt + self.rect_blit.y += self.config['ns'].speed * self.config['ns'].dt + + self.rect.bottomleft = self.rect_blit.bottomleft class TimeBoost(BoostMixin, pygame.sprite.Sprite): @@ -209,7 +214,7 @@ def __init__(self, screen, base_dir, config, plate, life=5): self.img_small = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_small.bmp') self.img_active = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_activate.bmp') - self.rect_active = Rect(self.img_active.get_rect()) + self.rect_active = Ellipse(self.img_active.get_rect()) BoostMixin.__init__(self, screen, base_dir, config, 'shield', life) From 4db7550aa1e0a9414e56a58ace031bd3a4a5db50 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 30 Oct 2021 18:27:36 +0300 Subject: [PATCH 10/31] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20(=D0=B8=D0=B7=20rect.py?= =?UTF-8?q?=20=D0=B2=20hitbox.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CODESTYLE.md | 4 ++-- spaceway/hitbox.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/CODESTYLE.md b/docs/CODESTYLE.md index 14267f5..324058b 100644 --- a/docs/CODESTYLE.md +++ b/docs/CODESTYLE.md @@ -10,9 +10,9 @@ Now project has this structure: |____ collection.py |____ config.py |____ debug.py +|____ hitbox.py |____ main.py |____ mixins.py -|____ rect.py |____ updater.py |____ assets | |____ @@ -33,9 +33,9 @@ General files: - *collection.py* - file with the implementation of additional data structures, mainly the *pygame.sprite.Group* extensions - *config.py* - file with some objects for easier configuration management - *debug.py* - file with some objects for easier debugging game +- *hitbox.py* - аile with implementation of hitboxes for some objects calculations - *main.py* - main file, import all modules, contains the entrypoint of game and connects all the scenes together - *mixins.py* - file with mixins which are needed for simple creation of the same type of objects (DRY principle) -- *rect.py* - file with implementation of a `pygame.Rect` for working with float values - *updater.py* - file responsible for updating Space Way Assets: diff --git a/spaceway/hitbox.py b/spaceway/hitbox.py index e2050db..5f11071 100644 --- a/spaceway/hitbox.py +++ b/spaceway/hitbox.py @@ -1,4 +1,5 @@ -""" File with extension of default `pygame.Rect` to use it with float values """ +""" File with implementation of hitboxes for calculating collisions, + position and other. Based on `pygame.Rect` """ from math import sqrt, atan2, pi, sin, cos From b48b0b61c9f2f64a19461076ff9abbc3bc6bd3ae Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sun, 31 Oct 2021 14:12:06 +0300 Subject: [PATCH 11/31] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=BE=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE=D0=BA=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/collection.py | 4 ++-- spaceway/scenes/game/objects.py | 8 -------- spaceway/scenes/lobby/objects.py | 6 ------ spaceway/scenes/settings/objects.py | 10 ---------- spaceway/scenes/table/objects.py | 2 -- 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/spaceway/collection.py b/spaceway/collection.py index bf3b5db..c85ff0f 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -137,7 +137,7 @@ def center(self) -> None: # Count width of all buttons for button in self: - buttons_width += button.width + buttons_width += button.rect.w # Calculate the width of all buttons with spaces between them all_width = buttons_width + self.SPACE * (len(self) - 1) @@ -150,7 +150,7 @@ def center(self) -> None: button.rect.x = x button.rect.centery = self.screen_height // 2 - x += button.width + self.SPACE + x += button.rect.w + self.SPACE def perform_point_collides(self, point: Tuple[int, int]) -> bool: """ Detects collisions of buttons with the specified point. If a diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index f3834ab..2a943ab 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -311,8 +311,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/resume.bmp') self.rect = Ellipse(self.img.get_rect()) @@ -324,8 +322,6 @@ def __init__(self, screen, base_dir, config, defeat, *defeat_args): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') self.rect = Ellipse(self.img.get_rect()) @@ -346,8 +342,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/again.bmp') self.rect = Ellipse(self.img.get_rect()) @@ -359,8 +353,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') self.rect = Ellipse(self.img.get_rect()) diff --git a/spaceway/scenes/lobby/objects.py b/spaceway/scenes/lobby/objects.py index 2484b7a..89d864e 100644 --- a/spaceway/scenes/lobby/objects.py +++ b/spaceway/scenes/lobby/objects.py @@ -9,8 +9,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 90 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/play.bmp') self.rect = Ellipse(self.img.get_rect()) @@ -33,8 +31,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/table.bmp') self.rect = Ellipse(self.img.get_rect()) @@ -57,8 +53,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/settings.bmp') self.rect = Ellipse(self.img.get_rect()) diff --git a/spaceway/scenes/settings/objects.py b/spaceway/scenes/settings/objects.py index a4d5c56..c9d41c2 100644 --- a/spaceway/scenes/settings/objects.py +++ b/spaceway/scenes/settings/objects.py @@ -6,8 +6,6 @@ class EffectsButton(SettingsButtonMixin): def __init__(self, screen, base_dir, config): - self.width = self.height = 63 - self.imgs = {True: pygame.image.load(f'{base_dir}/assets/images/buttons/effects_true.bmp'), False: pygame.image.load(f'{base_dir}/assets/images/buttons/effects_false.bmp')} @@ -16,8 +14,6 @@ def __init__(self, screen, base_dir, config): class FullScreenButton(SettingsButtonMixin): def __init__(self, screen, base_dir, config): - self.width = self.height = 63 - self.imgs = {True: pygame.image.load(f'{base_dir}/assets/images/buttons/full_screen_true.bmp'), False: pygame.image.load(f'{base_dir}/assets/images/buttons/full_screen_false.bmp')} @@ -32,8 +28,6 @@ def change_state(self): class UpdatesButton(SettingsButtonMixin): def __init__(self, screen, base_dir, config): - self.width = self.height = 63 - self.imgs = {True: pygame.image.load(f'{base_dir}/assets/images/buttons/updates_true.bmp'), False: pygame.image.load(f'{base_dir}/assets/images/buttons/updates_false.bmp')} @@ -42,8 +36,6 @@ def __init__(self, screen, base_dir, config): class DifficultyButton(SettingsButtonMixin): def __init__(self, screen, base_dir, config): - self.width = self.height = 63 - self.imgs = {0: pygame.image.load(f'{base_dir}/assets/images/buttons/difficulty_easy.bmp'), 1: pygame.image.load(f'{base_dir}/assets/images/buttons/difficulty_middle.bmp'), 2: pygame.image.load(f'{base_dir}/assets/images/buttons/difficulty_hard.bmp'), @@ -60,8 +52,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') self.rect = Ellipse(self.img.get_rect()) diff --git a/spaceway/scenes/table/objects.py b/spaceway/scenes/table/objects.py index 2d7ea95..8f119ad 100644 --- a/spaceway/scenes/table/objects.py +++ b/spaceway/scenes/table/objects.py @@ -57,8 +57,6 @@ def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() - self.width = self.height = 63 - self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/back.bmp') self.rect = Ellipse(self.img.get_rect()) From 579dc38a704a67f191c637f991c641d63a7cb267 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Mon, 1 Nov 2021 19:14:54 +0300 Subject: [PATCH 12/31] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=85=D0=BD=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=B8=20=D0=BD=D0=B8=D0=B6=D0=BD=D0=B5=D0=B9=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D0=BD=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=81=D0=B8=20Y=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BD=D0=BE?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=20=D1=81=D1=86=D0=B5=D0=BD=D1=8B=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/mixins.py | 33 ++++++++++++++++------------- spaceway/scenes/lobby/objects.py | 33 ++++++----------------------- spaceway/scenes/settings/objects.py | 12 +++-------- spaceway/scenes/table/objects.py | 12 +++-------- 4 files changed, 30 insertions(+), 60 deletions(-) diff --git a/spaceway/mixins.py b/spaceway/mixins.py index e4a43d1..d14192f 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -1,33 +1,39 @@ """ File with implementations of various mixins for easier creation of objects and following the DRY principle """ -from typing import Union +from typing import Literal from random import randint +from math import inf import pygame from .collection import SceneButtonsGroup -from .hitbox import Rect, Ellipse +from .hitbox import Ellipse class SceneButtonMixin(pygame.sprite.Sprite): """ Mixin for scene buttons, which can change current scene """ - def __init__(self, base_dir, config, scene: str, sub_scene: str, - change_scene_to: str, change_sub_scene_to: str, speed: int = 0, - action: Union['enter', 'leave', 'stop'] = 'stop') -> None: + def __init__(self, base_dir, config, scene: str, sub_scene: str, change_scene_to: str, + change_sub_scene_to: str, speed: float = 0, top: float = inf, bottom: float = inf, + action: Literal['enter', 'leave', 'stop'] = 'stop') -> None: """ Initialize the mixin anywhere in your `__init__` function. `scene` and `sub_scene` arguments determine which scene button belongs to. `change_scene_to` and `change_sub_scene_to` determine which scene - will be changed when the button is clicked. `speed` argument - defines speed of button movement. `action` argument defines first - action of button """ + will be changed when the button is clicked. `top` and `bottom` defines + boundaries of button position on Y axis (top < bottom). `speed` + argument defines speed of button movement. `action` argument defines + first action of button """ pygame.sprite.Sprite.__init__(self) # Set variables for next use self.config = config self.action = action + # Set top and bottom boundaries of button (on Y axis) + self.top = top + self.bottom = bottom + # If speed is positive, on `enter` event button will move up, # on `leave` event - down. If speed is negative, on `enter` event # button will move down, on `leave` event - up @@ -51,11 +57,13 @@ def update(self) -> None: # If button must move if self.action != 'stop': # Check, if move can be continued - if self.keep_move(): + inc = (self.speed if self.action == 'leave' else -self.speed) * self.config['ns'].dt + if self.top < self.rect.y + inc < self.bottom: # If can be, move button - self.rect.y += (self.speed if self.action == 'leave' else -self.speed) * self.config['ns'].dt + self.rect.y += inc else: # Else, stop button and call action callback + self.rect.y = min(max(self.rect.y + inc, self.top), self.bottom) if self.action == 'enter': self.post_enter() else: @@ -76,11 +84,6 @@ def leave(self, post_leave=lambda: None) -> None: self.action = 'leave' self.post_leave = post_leave - def keep_move(self) -> bool: - """ Function to control movement of button. It contains - conditions due of it decides, continue move or not """ - return False - def change_scene(self) -> None: """ Change scene to another one (that was defined in `__init__`) """ self.config['scene'] = self.change_scene_to diff --git a/spaceway/scenes/lobby/objects.py b/spaceway/scenes/lobby/objects.py index 89d864e..c3a7982 100644 --- a/spaceway/scenes/lobby/objects.py +++ b/spaceway/scenes/lobby/objects.py @@ -15,15 +15,8 @@ def __init__(self, screen, base_dir, config): self.rect.centerx = self.screen_rect.centerx self.rect.bottom = self.screen_rect.top - SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', - 'game', 'game', -16, 'enter') - - def keep_move(self): - if self.action == 'enter': - return self.rect.centery < self.screen_rect.centery - if self.action == 'leave': - return self.rect.bottom > self.screen_rect.top - return False + SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', 'game', 'game', + -16, self.rect.top, self.screen_rect.centery - self.rect.h / 2, 'enter') class TableButton(SceneButtonMixin): @@ -37,15 +30,8 @@ def __init__(self, screen, base_dir, config): self.rect.left = self.screen_rect.left + 5 self.rect.top = self.screen_rect.bottom - SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', - 'table', 'table', 4, 'enter') - - def keep_move(self): - if self.action == 'enter': - return self.rect.bottom > self.screen_rect.bottom - 5 - if self.action == 'leave': - return self.rect.top < self.screen_rect.bottom - return False + SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', 'table', 'table', + 4, self.screen_rect.bottom - self.rect.h - 5, self.rect.top, 'enter') class SettingsButton(SceneButtonMixin): @@ -59,15 +45,8 @@ def __init__(self, screen, base_dir, config): self.rect.right = self.screen_rect.right - 5 self.rect.top = self.screen_rect.bottom - SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', - 'settings', 'settings', 4, 'enter') - - def keep_move(self): - if self.action == 'enter': - return self.rect.bottom > self.screen_rect.bottom - 5 - if self.action == 'leave': - return self.rect.top < self.screen_rect.bottom - return False + SceneButtonMixin.__init__(self, base_dir, config, 'lobby', 'lobby', 'settings', 'settings', + 4, self.screen_rect.bottom - self.rect.h - 5, self.rect.top, 'enter') class Caption(CaptionMixin, pygame.sprite.Sprite): diff --git a/spaceway/scenes/settings/objects.py b/spaceway/scenes/settings/objects.py index c9d41c2..26e47b4 100644 --- a/spaceway/scenes/settings/objects.py +++ b/spaceway/scenes/settings/objects.py @@ -56,16 +56,10 @@ def __init__(self, screen, base_dir, config): self.rect = Ellipse(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 - self.rect.top = self.screen_rect.bottom - 5 + self.rect.top = self.screen_rect.bottom - SceneButtonMixin.__init__(self, base_dir, config, 'settings', 'settings', 'lobby', 'lobby', 4) - - def keep_move(self): - if self.action == 'enter': - return self.rect.bottom > self.screen_rect.bottom - 5 - if self.action == 'leave': - return self.rect.top < self.screen_rect.bottom - return False + SceneButtonMixin.__init__(self, base_dir, config, 'settings', 'settings', 'lobby', 'lobby', + 4, self.screen_rect.bottom - self.rect.h - 5, self.rect.top, 4) class NickInput: diff --git a/spaceway/scenes/table/objects.py b/spaceway/scenes/table/objects.py index 8f119ad..c0e7880 100644 --- a/spaceway/scenes/table/objects.py +++ b/spaceway/scenes/table/objects.py @@ -61,13 +61,7 @@ def __init__(self, screen, base_dir, config): self.rect = Ellipse(self.img.get_rect()) self.rect.left = self.screen_rect.left + 5 - self.rect.top = self.screen_rect.bottom - 5 + self.rect.top = self.screen_rect.bottom - SceneButtonMixin.__init__(self, base_dir, config, 'table', 'table', 'lobby', 'lobby', 4) - - def keep_move(self): - if self.action == 'enter': - return self.rect.bottom > self.screen_rect.bottom - 5 - if self.action == 'leave': - return self.rect.top < self.screen_rect.bottom - return False + SceneButtonMixin.__init__(self, base_dir, config, 'table', 'table', 'lobby', 'lobby', + 4, self.screen_rect.bottom - self.rect.h - 5, self.rect.top, 4) From 532b495c32869ebb04a977e4071417e91f57d098 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 5 Nov 2021 13:33:58 +0300 Subject: [PATCH 13/31] =?UTF-8?q?=D0=A1=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20SV?= =?UTF-8?q?G=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/icons/icon.svg | 318 +--------------------------------------- docs/icons/icon_fit.svg | 314 +-------------------------------------- setupfiles/spaceway.svg | 318 +--------------------------------------- 3 files changed, 3 insertions(+), 947 deletions(-) diff --git a/docs/icons/icon.svg b/docs/icons/icon.svg index 7a67a3c..1687b8d 100644 --- a/docs/icons/icon.svg +++ b/docs/icons/icon.svg @@ -1,317 +1 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/icons/icon_fit.svg b/docs/icons/icon_fit.svg index 5c5ef33..4fe82cf 100644 --- a/docs/icons/icon_fit.svg +++ b/docs/icons/icon_fit.svg @@ -1,313 +1 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/setupfiles/spaceway.svg b/setupfiles/spaceway.svg index 7a67a3c..1687b8d 100644 --- a/setupfiles/spaceway.svg +++ b/setupfiles/spaceway.svg @@ -1,317 +1 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file From be4951367cb142d867e3ad520ca0386f4502155d Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 5 Nov 2021 14:29:59 +0300 Subject: [PATCH 14/31] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5=D0=B6?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1dc74c7..c8b11a8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -29,6 +29,8 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Install dependencies - run: pip install . pytest + run: pip install -r requirements.txt pytest-cov - name: Run tests - run: pytest --color=yes + run: python -m pytest --cov spaceway --cov-report xml --color=yes tests/ + - name: Upload coverage + uses: codecov/codecov-action@v2 From e3d081d1bf7f6d8334d1a8904f562cdddf382c1d Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 5 Nov 2021 18:37:52 +0300 Subject: [PATCH 15/31] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=83=D1=80=D1=81=D0=B8=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20?= =?UTF-8?q?=D1=81=D1=86=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/__init__.py | 2 ++ tests/test_scenes.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/test_scenes.py diff --git a/spaceway/__init__.py b/spaceway/__init__.py index b272409..419d4c7 100644 --- a/spaceway/__init__.py +++ b/spaceway/__init__.py @@ -4,3 +4,5 @@ if version_info < MIN_PYTHON: raise Exception('Space Way requires Python {0}.{1}.{2} or newer'.format(*MIN_PYTHON)) + +from . import main diff --git a/tests/test_scenes.py b/tests/test_scenes.py new file mode 100644 index 0000000..a5abdf2 --- /dev/null +++ b/tests/test_scenes.py @@ -0,0 +1,23 @@ +import os + +import spaceway + + +ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) + '/' + + +def test_recursive_import(): + exclude_dirs = ('__pycache__',) + exclude_files = ('__init__.py',) + + for root, dirs, files in os.walk(os.path.dirname(spaceway.scenes.__file__)): + dirs[:] = [d for d in dirs if d not in exclude_dirs] + files[:] = [f for f in files if f.endswith('.py') and f not in exclude_files] + + for imp in dirs: + obj = root.replace(ROOT_DIR, '').replace('/', '.') + '.' + imp + assert obj in dir(spaceway.scenes) + + for imp in files: + obj = root.replace(ROOT_DIR, '').replace('/', '.') + '.' + imp[:-3] + assert obj in dir(spaceway.scenes) From 0435ab28d4cb2ec55b8046d4998e676f3559fb5c Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sun, 7 Nov 2021 16:17:49 +0300 Subject: [PATCH 16/31] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82-=D1=81=D1=82=D1=80=D0=BE=D0=BA=20=D0=B2=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B5=20Sphinx=20(Napoleon)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/collection.py | 254 +++++++++++++++++++++++++-------------- spaceway/config.py | 76 ++++++------ spaceway/debug.py | 94 +++++++++------ spaceway/mixins.py | 263 +++++++++++++++++++++++++---------------- 4 files changed, 427 insertions(+), 260 deletions(-) diff --git a/spaceway/collection.py b/spaceway/collection.py index c85ff0f..74ec0d9 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -1,21 +1,27 @@ """ File with implementations of additional data structures """ -from typing import Union, List, Tuple, Dict +from typing import List, Dict import pygame class BoostsGroup(pygame.sprite.Group): - """ Extension of default pygame.sprite.Group for more easier control - of boosts. Boosts are stored in two groups: active (which were - activated) and passive (which were not activated). Active group - cannot contain more than one boost of one type. Boosts are stored - in form name-boost: + """Extension of default :group:`pygame.sprite.Group` for more + easier control of boosts + + Args: + The same as the pygame :group:`pygame.sprite.Group` + + Note: + Boosts are stored in two groups: active (which were activated) and passive + (which weren't activated). Active group cannot contain more than one boost + of one type, because they are stored in the following format: + .. code:: python {'time': } - Passive group contains other boosts in spritedict-likely style - (boost-0): + Passive group contains other boosts in in the following format: + .. code:: python {: 0} """ @@ -26,18 +32,23 @@ class BoostsGroup(pygame.sprite.Group): # Define interval for next boost spawn (in score) next_spawn = 3 - def add_internal(self, boost: 'BoostMixin') -> None: - """ Adds boost to passive group """ + def add_internal(self, boost) -> None: + """Adds boost to passive group + Args: + boost (spaceway.mixins.BoostMixin): boost to be added to group + """ self.passive[boost] = 0 pygame.sprite.Group.add_internal(self, boost) - def remove_internal(self, boost: 'BoostMixin') -> None: - """ Removes boost. If boost is located in passive group, - it simply will remove it from group. If boost is located - in active group, it will update number in queue of other - boosts and remove boost from group """ + def remove_internal(self, boost) -> None: + """Removes boost. If boost is located in passive group, it simply will + remove it from group. If boost is located in active group, it will update + number in queue of other boosts and then remove boost from group + Args: + boost (spaceway.mixins.BoostMixin): Boost to be removed from group + """ # If boost was activated if self.get(boost.name) == boost: # Selected boost was not processed @@ -61,8 +72,8 @@ def remove_internal(self, boost: 'BoostMixin') -> None: pygame.sprite.Group.remove_internal(self, boost) def empty(self) -> None: - """ Resets itself """ - + """Resets itself and removes all boosts + """ # Reset default pygame group pygame.sprite.Group.empty(self) @@ -73,19 +84,31 @@ def empty(self) -> None: # Reset `next_spawn` self.next_spawn = 3 - def __contains__(self, item: Union[str, 'BoostMixin']) -> bool: - """ Will return True, if group contains activated - boost with passed name, else - False """ + def __contains__(self, boost): + """Check if boost contains in this group + + Args: + boost (Union[str, spaceway.mixins.BoostMixin]): Boost to be checked + whether it's contained in group. If argument is a string, boost will + be checked in active group, otherwise among all groups + + Returns: + bool: Is boost contained in the group or not + """ + if isinstance(boost, str): + return bool(self.get(boost)) + return self.has(boost) - if isinstance(item, str): - return bool(self.get(item)) - return self.has(item) + def activate(self, boost) -> None: + """Activates passed boost and move boost from passive group to active - def activate(self, boost: 'BoostMixin') -> None: - """ Activates passed boost and move boost from passive group - to active. If boost with boost's name have already activated, - it will nullify tick (boost timer will start again) """ + Args: + boost (spaceway.mixins.BoostMixin): Boost to be activated + Important: + If some boost with boost's name have already activated, it will nullify + tick of already activated boost (boost timer will start again) + """ # Set `number_in_queue` like last boost in queue boost.number_in_queue = len(self.active) + 1 @@ -100,30 +123,41 @@ def activate(self, boost: 'BoostMixin') -> None: self.active[boost.name] = boost boost.activate() - def get(self, name: str) -> Union['BoostMixin', None]: - """ Will return boost if active group contains boost - with passed name. Else it will return `None` """ + def get(self, name): + """Searche for a boost with the passed name in active group and, + if there is one, returns it + Args: + name (str): Name of boost + + Returns: + Union[spaceway.mixins.BoostMixin, None]: Will return boost if + active group contains it, otherwise `None` + """ return self.active.get(name) class CenteredButtonsGroup(pygame.sprite.Group): - """ Extension of pygame.sprite.Group for centering group of - buttons on screen. Requires an additional parameter during - initialization `mode` (list or tuple with sizes of screen). - When you add or remove buttons, the group is centered again. - For each button, you must specify its dimensions, e.g.: + """Extension of default :group:`pygame.sprite.Group` for centering + buttons of group on screen + + Args: + mode (Tuple[int, int]): The size of the :surface:`pygame.Surface` + relative to which the buttons will be centered + *buttons (pygame.sprite.Sprite): Buttons to be added to the group - self.width = 10 - self.height = 15 """ + Note: + Centering of buttons occurs during the addition/removal of + a button from a group + """ # Define space between buttons (px) SPACE = 7 - def __init__(self, mode: Tuple[int, int], *buttons: List[pygame.sprite.Sprite]) -> None: - """ Initialization of group: adding buttons and setting - of width and height of screen """ - + def __init__(self, mode, *buttons): + """Constructor method. Adding buttons and setting of width and + height of surface + """ # Initialize inherited group pygame.sprite.Group.__init__(self, *buttons) @@ -131,8 +165,8 @@ def __init__(self, mode: Tuple[int, int], *buttons: List[pygame.sprite.Sprite]) self.screen_width, self.screen_height = mode def center(self) -> None: - """ Centering of group """ - + """Centering of group + """ buttons_width = 0 # Count width of all buttons @@ -145,18 +179,23 @@ def center(self) -> None: # Starting point for x x = (self.screen_width - all_width) // 2 - # Update rectangles of buttons + # Update hitboxes of buttons for button in self: button.rect.x = x button.rect.centery = self.screen_height // 2 x += button.rect.w + self.SPACE - def perform_point_collides(self, point: Tuple[int, int]) -> bool: - """ Detects collisions of buttons with the specified point. If a - collision was found, it presses on the button and returns `True`, - otherwise it simply returns `False` """ + def perform_point_collides(self, point): + """Detects collisions of buttons with the specified point. If a + collision was found, it presses on the button + Args: + point (Tuple[int, int]): The point at which the collision is checked + + Returns: + bool: Has a collision been found + """ # Check all buttons for button in self: if button.rect.collidepoint(point): @@ -167,55 +206,59 @@ def perform_point_collides(self, point: Tuple[int, int]) -> bool: # Return `False` if no collisions were found return False - def add_internal(self, button: pygame.sprite.Sprite) -> None: - """ Adding button and centering of group """ + def add_internal(self, button) -> None: + """Adding button and centering of group + Args: + button (pygame.sprite.Sprite): Button to be added to the group + """ pygame.sprite.Group.add_internal(self, button) self.center() - def remove_internal(self, button: pygame.sprite.Sprite) -> None: - """ Removing button and centering of group """ + def remove_internal(self, button) -> None: + """Removing button and centering of group + Args: + button (pygame.sprite.Sprite): Button to be removed from the group + """ pygame.sprite.Group.remove_internal(self, button) self.center() def draw(self) -> None: - """ Updates and blits all buttons of group """ - + """Updates and blits all buttons of group + """ for button in self: button.update() button.blit() class SceneButtonsGroup(pygame.sprite.Group): - """ Extension of pygame.sprite.Group for easier control of - scenes buttons. It has got dictionary with buttons and - a structure like this: - - buttons = { - sceneN: { - sub_scene1: [SceneButtonMixin, ...], - ... - }, - ... - } """ + """Extension of default :group:`pygame.sprite.Group` for easier control + of scene buttons + + Args: + config (spaceway.config.ConfigManager): The configuration object + *buttons (pygame.sprite.Sprite): Buttons to be added to the group + """ # Define an additional dictionary for structuring buttons by scenes buttons: Dict[str, Dict[str, List['SceneButtonMixin']]] = {} - def __init__(self, config, *buttons: List['SceneButtonMixin']) -> None: - """ Initialization of group. Pass `config` argument and list of buttons - `buttons` to add them to group """ - + def __init__(self, config, *buttons): + """Constructor method + """ # Initialization of inherited group pygame.sprite.Group.__init__(self, *buttons) # Set `config` for the further use self.config = config - def add_internal(self, button: 'SceneButtonMixin') -> None: - """ Adding button to group and structuring by scene """ + def add_internal(self, button) -> None: + """Adding button to group and structuring by scene + Args: + button (pygame.sprite.Sprite): Button to be added to the group + """ # If there were not buttons with scene of current button yet if self.buttons.get(button.scene) is None: self.buttons[button.scene] = dict() @@ -229,19 +272,28 @@ def add_internal(self, button: 'SceneButtonMixin') -> None: pygame.sprite.Group.add_internal(self, button) - def remove_internal(self, button: 'SceneButtonMixin') -> None: - """ Remove button from group. It is assumed that button has already been added """ + def remove_internal(self, button) -> None: + """Remove button from group + Args: + button (pygame.sprite.Sprite): Button to be removed from the group + """ # Remove button from group self.buttons[button.scene][button.sub_scene].remove(button) pygame.sprite.Group.remove_internal(self, button) - def perform_point_collides(self, point: Tuple[int, int]) -> bool: - """ Detects button collisions with the specified point. If collision - was found, presses the button, leaves buttons of current scenу - and enters buttons of scene to which it will be changed """ + def perform_point_collides(self, point): + """Detects collisions of buttons with the specified point. If a collision + was found, presses the button, leaves buttons of current scene and enters + buttons of scene to which it will be changed + Args: + point (Tuple[int, int]): The point at which the collision is checked + + Returns: + bool: Has a collision been found + """ # Get all buttons of current scene for button in self.get_by_scene(): # If collision was found @@ -258,38 +310,60 @@ def perform_point_collides(self, point: Tuple[int, int]) -> bool: # No collisions were found return False - def enter_buttons(self, scene: str = '', sub_scene: str = '') -> None: - """ Enters all buttons of selected scene. If no scene was selected, - buttons of current scene will be entered """ + def enter_buttons(self, scene='', sub_scene='') -> None: + """Enters all buttons of passed scene. If no scene was passed, + buttons of current scene will be entered + Args: + scene (Optional[str]): Buttons of specific scene which must be entered + sub_scene (Optional[str]): Buttons of specific subscene which must be entered + """ for button in self.get_by_scene(scene, sub_scene): button.enter() - def leave_buttons(self, scene: str = '', sub_scene: str = '') -> None: - """ Leaves all buttons of selected scene. If no scene was selected, - buttons of current scene will be left """ + def leave_buttons(self, scene='', sub_scene='') -> None: + """Leaves all buttons of passed scene. If no scene was passed, + buttons of current scene will be left + Args: + scene (Optional[str]): Buttons of specific scene which must be left + sub_scene (Optional[str]): Buttons of specific subscene which must be left + """ for button in self.get_by_scene(scene, sub_scene): button.leave() def draw(self) -> None: - """ Updates and blits all buttons of group """ - + """Updates and blits all buttons of group + """ for button in self.get_by_scene(): button.update() button.blit() - def get_by_scene(self, scene: str = '', sub_scene: str = '') -> List['SceneButtonMixin']: - """ Returns all buttons of selected scene. If no scene was selected, - buttons of current scene will be returned """ + def get_by_scene(self, scene='', sub_scene=''): + """Returns all buttons of passed scene. If no scene was selected, + buttons of current scene will be returned + Args: + scene (Optional[str]): Buttons of specific scene which must be returned + sub_scene (Optional[str]): Buttons of specific subscene which must be returned + + Returns: + List[pygame.sprite.Sprite]: List of buttons of specific scene + """ return self.buttons \ .get(scene or self.config['scene'], {}) \ .get(sub_scene or self.config['sub_scene'], []) - def get_by_instance(self, instance: any) -> Union['SceneButtonMixin', None]: - """ Returns only one button or `None` which is an instance of passed class """ + def get_by_instance(self, instance): + """Returns first button which is an instance of passed class + + Args: + instance (any): Instance from which button must be inherited + Returns: + Union[pygame.sprite.Sprite, None]: Button object, if there is one, + otherwise `None` + """ for button in self: if isinstance(button, instance): # Return button if it is an instance of passed class diff --git a/spaceway/config.py b/spaceway/config.py index 679b52f..4cbd952 100644 --- a/spaceway/config.py +++ b/spaceway/config.py @@ -8,36 +8,46 @@ class Namespace: - """ Stores variables that do not need to be imported or exported - Since `ConfigManager` is passed everywhere, it allows you to - safely exchange variables from different functions and even - scenes """ + """Stores variables that do not need to be imported or exported. Since + :ConfigManager:`spaceway.config.ConfigManager` is passed everywhere, it + allows you to safely exchange variables from different functions and even + scenes + """ pass class ConfigManager(dict): - """ Configuration manager. Inherited from `dict` class and can be - used as default dictionary. Different configuration files are - mounted to different points of dictionary, e.g.: - - config.json -> `root dictionary` + """Configuration manager. Inherited from `dict` class and can be used + as default dictionary + + Args: + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + + Important: + Manager can use original configurations (which come with the package) or + user configurations. Use original configurations for debugging (they are + easier to edit) and don't use them for release (because with update o + package they will be changed) and user progress will not be saved. Use + user configurations for release, because if they are created they won't + be changed to new ones + + Note: + Different configuration files are mounted to different points of + dictionary, e.g.: + :: + + config.json -> *root dictionary* user.json -> 'user' score.csv -> 'score_list' - - Manager can use original configurations (which come with the - package) or user configurations. Use original configurations - for debugging (they are easier to edit) and do not use them - for release (because with update of package they will be changed) - and user progress will not be saved. Use user configurations for - release, because if they are created they will not be changed - to new ones """ + """ # Set `True` to use configurations in user directory, or `False` for package directory USE_USER_CONFIGS = True def __init__(self, base_dir) -> None: - """ Initializing of ConfigManager """ - + """Initializing of ConfigManager + """ # Setting BASE_DIR const for the further use self.BASE_DIR = base_dir @@ -57,11 +67,10 @@ def __init__(self, base_dir) -> None: self.__load() def __check_configs(self) -> None: - """ Сhecks whether the configurations have been created and copies - them to the user's directory if not (if USE_USER_CONFIGS = `True`) - Replacing user configuration paths with original configuration - paths (if USE_USER_CONFIGS = `False`) """ - + """Сhecks whether the configurations have been created and copies them to the + user's directory if not (if USE_USER_CONFIGS = `True`) Replacing user configuration + paths with original configuration paths (if USE_USER_CONFIGS = `False`) + """ # Checking whether configurations were created and copying its to # user configurations directory if not if self.USE_USER_CONFIGS: @@ -79,8 +88,8 @@ def __check_configs(self) -> None: self.PATH_SCORE_CONFIG = self.__ORIGINAL_PATH_SCORE_CONFIG def __load(self) -> None: - """ Loading all configurations and initializing ConfigManager as dictionary """ - + """Loading all configurations and initializing ConfigManager as dictionary + """ # Set root dictionary from main configuration with open(self.PATH_MAIN_CONFIG) as file: config: dict = load(file) @@ -106,8 +115,8 @@ def __load(self) -> None: dict.__init__(self, config) def save(self) -> None: - """ Saves all configurations """ - + """Saves all configurations + """ # Saving user configuration with open(self.PATH_USER_CONFIG, 'w') as file: dump(self['user'], file, indent=4) @@ -121,8 +130,8 @@ def save(self) -> None: file.write(','.join((str(score), nick)) + '\n') def reset(self) -> None: - """ Resets user configurations, replacing them with default configurations """ - + """Resets user configurations, replacing them with default configurations + """ if self.USE_USER_CONFIGS: # Loading default configurations ConfigManager.USE_USER_CONFIGS = False @@ -142,8 +151,7 @@ def reset(self) -> None: self.save() def filter_score(self) -> None: - """ Fiters scores of attempts. Attempts are sorted by best - score and then all other attempts are discarded so that - only the top 5 attempts remain """ - + """Fiters scores of attempts. Attempts are sorted by best score and then + all other attempts are discarded so that only the top 5 attempts remain + """ self['score_list'] = list(reversed(sorted(self['score_list'])))[:5] diff --git a/spaceway/debug.py b/spaceway/debug.py index da83c50..c21476e 100644 --- a/spaceway/debug.py +++ b/spaceway/debug.py @@ -9,38 +9,48 @@ class DebugModule: - """ Debug module - part of Debugger. Every debug module must have at least - three functions: `__init__`, `interval_update`, `static_update`. I could - have written all the modules directly in the Debugger, but I didn't do - this to get rid of the confusion in the code and to modify them more - easily, so each module should perform a specific task """ + """Debug module - part of Debugger. Every debug module must have at least three + functions: `__init__`, `interval_update`, `static_update`. I could have written + all the modules directly in the Debugger, but I didn't do this to get rid of the + confusion in the code and to modify them more easily, so each module should + perform a specific task + """ def __init__(self, *args, **kwargs) -> None: - """ Function is called only once when the module is enabled. Here - the module gets and saves certain objects for further use and - performs the configuration itself """ + """Function is called only once when the module is enabled. Here the module gets + and saves certain objects for further use and performs the configuration itself + """ pass def interval_update(self) -> None: - """ Function is called after a certain time interval, - which defined in the Debugger """ + """Function is called after a certain time interval, + which defined in the Debugger + """ pass def static_update(self) -> None: - """ Function is called on each iteration of game loop """ + """Function is called on each iteration of game loop + """ pass class DebugStat(DebugModule): - """ Debug module for viewing the current state of CPU usage, RAM, - and other game and system information in the lower-left corner """ + """Debug module for viewing the current state of CPU usage, RAM, + and other game and system information in the lower-left corner + """ # Color of information text COLOR = (158, 46, 255) def __init__(self, screen, base_dir, clock) -> None: - """ Initializes the module, saving objects and configuring itself """ - + """Initializes the module, saving objects and configuring itself + + Args: + screen (pygame.Surface): Screen (surface) obtained via pygame + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + clock (pygame.time.Clock): Clock object obtained via pygame + """ self.screen = screen self.screen_rect = self.screen.get_rect() @@ -51,9 +61,9 @@ def __init__(self, screen, base_dir, clock) -> None: self.clock = clock def interval_update(self) -> None: - """ In current module, this function is used to update the - text of the debugging information """ - + """In current module, this function is used to update the text + of the debugging information + """ # Creating list of messages as plain text self.msgs = ( f'FPS: {round(self.clock.get_fps(), 5)}', @@ -87,24 +97,29 @@ def interval_update(self) -> None: y -= 17 def static_update(self) -> None: - """ In current module, this function is used for - blitting information messages """ - + """In current module, this function is used for + blitting information messages + """ # Blitting debug information messages for i in range(len(self.msgs)): self.screen.blit(self.imgs[i], self.rects[i]) class DebugHitbox(DebugModule): - """ Debug module for drawing hitbox of every image """ + """Debug module for drawing hitbox of every image + """ # Color of hitbox COLOR_RECT = (0, 255, 0) COLOR_ELLIPSE = (0, 255, 255) def __init__(self, screen) -> None: - """ Initializes the module. Replaces the default `__init__` - function of `Hitbox` to track its instances """ + """Initializes the module. Replaces the default `__init__` + function of `Hitbox` to track its instances + + Args: + screen (pygame.Surface): Screen (surface) obtained via pygame + """ # Saving screen for the further use self.screen = screen @@ -121,9 +136,9 @@ def __init__(self, screen) -> None: @staticmethod def __hitbox_init(self, *args, **kwargs): - """ Loading image via default loading images function - and adding to this image hitbox """ - + """Initializing :hitbox:`spaceway.hitbox.Hitbox` with default method + and adding hitbox to list to track its + """ # Adding hitbox to list hitboxes.append(ref(self, hitboxes.remove)) @@ -131,6 +146,8 @@ def __hitbox_init(self, *args, **kwargs): return origin_hitbox_init(self, *args, **kwargs) def static_update(self): + """Blitting all hitboxes on a given surface (screen) + """ for hitbox in self.hitboxes: if isinstance(hitbox(), Ellipse): pygame.draw.ellipse(self.screen, self.COLOR_ELLIPSE, hitbox(), 1) @@ -139,28 +156,37 @@ def static_update(self): class Debugger: - """ Debugger class, manages debug modules """ + """Debugger class, manages debug modules + """ # Interval for calling `interval_update` of modules (in seconds) UPDATE_INTERVAL = 0.5 def __init__(self, FPS) -> None: - """ Initializing of Debugger, configuring itself """ + """Initializing of Debugger, configuring itself + Args: + FPS (int): The FPS of the game is defined in the configuration + """ # Setting objects for further using self.__modules = [] self.__tick = 0 self.__FPS = FPS - def enable_module(self, module: DebugModule, *args, **kwargs) -> None: - """ Enables a debug module, which must be inherited from `DebugModule` - Passes all arguments to the module for it configuring """ + def enable_module(self, module, *args, **kwargs) -> None: + """Enables a debug module + Args: + module (spaceway.debug.DebugModule): Module object which should + be initialized + *args (any): Arguments needed to initialize the module + **kwargs (any): Keyword arguments needed to initialize the module + """ self.__modules.append(module(*args, **kwargs)) def update(self) -> None: - """ Updates debug modules """ - + """Updates debug modules + """ # If `tick` overflow, reset it if self.__tick == 10 * self.__FPS: self.__tick = 0 diff --git a/spaceway/mixins.py b/spaceway/mixins.py index d14192f..15b6e14 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -1,7 +1,6 @@ """ File with implementations of various mixins for easier creation of objects and following the DRY principle """ -from typing import Literal from random import randint from math import inf @@ -12,31 +11,42 @@ class SceneButtonMixin(pygame.sprite.Sprite): - """ Mixin for scene buttons, which can change current scene """ - - def __init__(self, base_dir, config, scene: str, sub_scene: str, change_scene_to: str, - change_sub_scene_to: str, speed: float = 0, top: float = inf, bottom: float = inf, - action: Literal['enter', 'leave', 'stop'] = 'stop') -> None: - """ Initialize the mixin anywhere in your `__init__` function. `scene` - and `sub_scene` arguments determine which scene button belongs to. - `change_scene_to` and `change_sub_scene_to` determine which scene - will be changed when the button is clicked. `top` and `bottom` defines - boundaries of button position on Y axis (top < bottom). `speed` - argument defines speed of button movement. `action` argument defines - first action of button """ + """Mixin for scene buttons, which can change current scene. The buttons + can change position (Y axis only) when the scene changes. Mixin can be + initialized anywhere in your `__init__` function + + Args: + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + config (spaceway.config.ConfigManager): The configuration object + scene (str): The scene that the button belongs to + sub_scene (str): The subscene that the button belongs to + change_scene_to (str): The scene to switch to after pressing the button + change_sub_scene_to (str): The subscene to switch to after pressing the button + speed (Optional[float]): The speed of changing the position of the button (px/frame). + If the parameter is positive, on button entering, it will move down, if it is + negative, it will move up. On leaving button will move in the opposite + direction from the entering. Defaults to 0 (button doesn't move) + top (Optional[float]): The top limit of the button position. At the end of the + movement, button will be adjacent to it + bottom (Optional[float]): The bottom limit of the button position. At the end of + the movement, button will be adjacent to it + action (Optional[Literal["enter", "leave", "stop"]]): The action of button during + initialization - one of *enter*, *leave* or *stop*, defaults to *stop* + """ + + def __init__(self, base_dir, config, scene, sub_scene, change_scene_to, + change_sub_scene_to, speed=0, top=inf, bottom=inf, + action='stop'): + """Constructor method + """ pygame.sprite.Sprite.__init__(self) # Set variables for next use self.config = config self.action = action - - # Set top and bottom boundaries of button (on Y axis) self.top = top self.bottom = bottom - - # If speed is positive, on `enter` event button will move up, - # on `leave` event - down. If speed is negative, on `enter` event - # button will move down, on `leave` event - up self.speed = speed # Set events callbacks @@ -52,8 +62,8 @@ def __init__(self, base_dir, config, scene: str, sub_scene: str, change_scene_to self.change_sub_scene_to = change_sub_scene_to def update(self) -> None: - """ Update button position """ - + """Update button position + """ # If button must move if self.action != 'stop': # Check, if move can be continued @@ -62,7 +72,7 @@ def update(self) -> None: # If can be, move button self.rect.y += inc else: - # Else, stop button and call action callback + # Else, stop button, align it and call action callback self.rect.y = min(max(self.rect.y + inc, self.top), self.bottom) if self.action == 'enter': self.post_enter() @@ -71,31 +81,45 @@ def update(self) -> None: self.action = 'stop' def blit(self) -> None: - """ Blit button """ + """Blit button + """ self.screen.blit(self.img, self.rect) def enter(self, post_enter=lambda: None) -> None: - """ Start `enter` action and set `post_enter` callback for next use """ + """Start *enter* action + + Args: + post_enter (Optional[callable]): The callback that will be called after + the *enter* action is completed, defaults to `lambda: None` + """ self.action = 'enter' self.post_enter = post_enter def leave(self, post_leave=lambda: None) -> None: - """ Start `leave` action and set `post_leave` callback for next use """ + """Start *leave* action + + Args: + post_enter (Optional[callable]): The callback that will be called after + the *leave* action is completed, defaults to `lambda: None` + """ self.action = 'leave' self.post_leave = post_leave def change_scene(self) -> None: - """ Change scene to another one (that was defined in `__init__`) """ + """Change scene to another one that was defined during initialization + """ self.config['scene'] = self.change_scene_to self.config['sub_scene'] = self.change_sub_scene_to def press(self) -> None: - """ Сallback of button that is performed when it is pressed """ - - # Find `SceneButtonsGroup` button belongs to + """Сallback of button that is performed when it is pressed. Starts + *leave* action for buttons of the current scene and *enter* action for + buttons of the future scene + """ + # Find group :class:`spaceway.mixins.SceneButtonsGroup` button belongs to for group in self.groups(): if isinstance(group, SceneButtonsGroup): - # Leave buttons of current scene, and enter of next + # Leave buttons of the current scene, and enter of the next group.leave_buttons() group.enter_buttons(self.change_scene_to, self.change_sub_scene_to) break @@ -104,16 +128,21 @@ def press(self) -> None: class CaptionMixin: - """ Mixin for more convenient header creation. - Automatically selects color of border for caption """ - - def __init__(self, base_dir, config, caption: str) -> None: - """ Initializing the mixin. if you redefine the `__init__` function - call the `__init__` function of `CaptionMixin at the end of - your `__init__` function. Pass `caption` argument with text - of caption (it also can be a format string) """ - - # Setting variables for later use + """Mixin for creating headers. Automatically selects color of the border + defined by the user. Must be initialized at the bottom of your `__init__` + function + + Args: + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + config (spaceway.config.ConfigManager): The configuration object + caption (str): Plain or format (for dynamic captions) string - text of caption + """ + + def __init__(self, base_dir, config, caption): + """Contructor method + """ + # Setting variables for the further use self.config = config self.caption = caption @@ -129,21 +158,30 @@ def __init__(self, base_dir, config, caption: str) -> None: # Calling `update` function for generating all images self.update() - def update(self, *fields) -> None: - """ Update image (text) of caption and its border. Note that - this function will recreate `rect` and previous position - will be deleted (overwritten). Define `locate` function - to update `rect` position. if you redefine this function, - it must be called inside your function anywhere. Pass - arguments for caption, if `caption` is format string """ + def update(self, *args, **kwargs) -> None: + """Update text of caption and its border. Three kinds of color of border + correspond to #0099FF, #FC0FC0 and #00FF00 + Args: + *args (any): Pass arguments if you are using caption text as format string + **kwargs (any): Pass keyword arguments if you are using caption text as format string + + Note: + Don't forget to call this function if you are redefining it (it must be called + inside your function anywhere) + + Important: + This function will recreate :class:`pygame.Rect` for this caption and previous + position will be deleted (overwritten). Define `locate` function to change *rect* + position after update + """ # Render text of caption - self.img = self.font.render(self.caption.format(*fields), True, self.fg_color) + self.img = self.font.render(self.caption.format(*args, **kwargs), True, self.fg_color) # Render borders of different colors - self.colors = [self.font.render(self.caption.format(*fields), True, (0, 153, 255)), - self.font.render(self.caption.format(*fields), True, (252, 15, 192)), - self.font.render(self.caption.format(*fields), True, (0, 255, 0))] + self.colors = [self.font.render(self.caption.format(*args, **kwargs), True, (0, 153, 255)), + self.font.render(self.caption.format(*args, **kwargs), True, (252, 15, 192)), + self.font.render(self.caption.format(*args, **kwargs), True, (0, 255, 0))] # Recreate rect of text self.rect = self.img.get_rect() @@ -152,8 +190,8 @@ def update(self, *fields) -> None: self.locate() def blit(self) -> None: - """ Blit of caption in two steps: border, then text. """ - + """Blit of caption in two steps: border, then text + """ # Creating border: text of selected color is drawn with indents # (size of border) in four directions: up, right, down, and left self.screen.blit(self.colors[self.config['user']['color']], (self.rect.x + self.border, self.rect.y)) @@ -165,23 +203,32 @@ def blit(self) -> None: self.screen.blit(self.img, self.rect) def locate(self) -> None: - """ Change `rect` position. If you don't override this function, - caption will be located in the upper corner """ + """Change *rect* position. If you don't override this function, + caption will be located in the upper corner + """ pass class SettingsButtonMixin(pygame.sprite.Sprite): - """ Mixin for creating settings buttons. Simplifies the work by - automatically changing the state and image of the button """ + """Mixin for creating settings buttons. Automatically changes the state + and image of the button. Must be initialized at the bottom of your `__init__` + function - def __init__(self, screen, config, config_index: str) -> None: - """ Intialize mixin at the end of your `__init__` function. - Pass `config_index` argument, which means the key in the - configuration (config['user'][config_index]). Also define - an `imgs` dictionary with images for a specific state, e.g.: + Args: + screen (pygame.Surface): Screen (surface) obtained via pygame + config (spaceway.config.ConfigManager): The configuration object + config_index (str): Key of the configuration (name of the state) - self.imgs = {state1: pygame.Surface, state2: pygame.Surface ...} """ + Important: + You should define an *imgs* dictionary with images for all states, e.g.: + .. code:: python + self.imgs = {state1: pygame.Surface, state2: pygame.Surface ...} + """ + + def __init__(self, screen, config, config_index): + """Constructor method + """ pygame.sprite.Sprite.__init__(self) # Setting variables for the further use @@ -191,78 +238,86 @@ def __init__(self, screen, config, config_index: str) -> None: self.config = config self.config_index = config_index - # Getting state from configuration by `config_index` + # Getting state from configuration by *config_index* self.state = self.config['user'][self.config_index] - # Setting image by current state and getting its rectangle + # Setting image by current state and getting its hitbox self.img = self.imgs[self.state] self.rect = Ellipse(self.img.get_rect()) def change_state(self) -> None: - """ Changes state of button. By default it has on-off behaviour """ + """Changes state of button. By default it has on-off behaviour. Override + method for another behaviour + """ self.state = not self.state def update(self) -> None: - """ Update button: synchronize image and configuration with button state """ + """Update button: synchronize image and configuration with button state + """ self.img = self.imgs[self.state] self.config['user'][self.config_index] = self.state def blit(self) -> None: - """ Blit button """ + """Blit button + """ self.screen.blit(self.img, self.rect) def press(self) -> None: - """ Press callback of button. Changes self state and updates itself """ + """Press callback of button. Changes self state and updates itself + """ self.change_state() self.update() class BoostMixin: - """ Mixin for easier creation of boosts """ - - def __init__(self, screen, base_dir, config, name: str, life: int) -> None: - """ Initialize of boost. Initialize it at the end of your `__init__` - function. Pass `name` to define name of boost. Pass `life` to - define lifetime of your boost (in seconds). Previously define - `img_idle` (float image that is displayed before activation) and - `img_small` (displayed in the upper-left corner after activation) """ - - # Setting `screen` for the further use + """Mixin for creating boosts. Must be initialized at the bottom of your + `__init__` function + + Args: + screen (pygame.Surface): Screen (surface) obtained via pygame + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + config (spaceway.config.ConfigManager): The configuration object + name (str): Name of boost (defines a type of button) + life (float): Lifetime of boost (in seconds) + + Important: + You must previously define :img_idle:`pygame.Surface` (moving image + that is displayed before activation) and :img_small:`pygame.Surface` + (displayed in the upper-left corner after activation) + """ + + COLOR_LONG = (255, 255, 255) # Color of lifetime if there are a lot of + COLOR_SHORT = (255, 0, 0) # Color of lifetime if there are a few of + + def __init__(self, screen, base_dir, config, name, life): + """Constructor method + """ + # Setting variables for the further use self.screen = screen self.screen_rect = self.screen.get_rect() - # Setting `config` for the further use self.config = config - - # Color of time left when there is a lot of time left - self.fg_color = (255, 255, 255) - - # Color of time left when there is little time left - self.bg_color = (255, 0, 0) - - # Setting `font` for the further use self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 28) - # Setting variables for the further use self.name = name self.life = life self.is_active = False self.tick = 0 - # Generating a rectangle of `img_idle` and randomly positioning it + # Generating a hitbox of :img_idle:`pygame.Surface` and randomly positioning it self.rect_idle = Ellipse(self.img_idle.get_rect()) self.rect_idle.y = randint(self.screen_rect.top, self.screen_rect.bottom - self.rect_idle.height - 2) self.rect_idle.left = self.screen_rect.right - self.rect = self.rect_idle - # Generating a rectangle of `img_small` and positioning it at the upper-left corner + # Generating a rect of :img_small:`pygame.Surface` and positioning it at the upper-left corner self.rect_small = self.img_small.get_rect() self.rect_small.left = self.screen_rect.left + 2 def update(self) -> None: - """ Updates boost """ - + """Updates boost + """ # If boost was activated if self.is_active: # Vertical positioning of boost, taking into account the number in the boost queue @@ -270,14 +325,14 @@ def update(self) -> None: # Generating text with the remaining lifetime if (self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1 <= 3: - # Rendering text using `bg_border`, if there is little time left - self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.bg_color) + # Rendering text using *COLOR_SHORT*, if there is little time left + self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.COLOR_SHORT) self.rect_life = self.img_life.get_rect() self.rect_life.top = self.screen_rect.top + 2 * self.number_in_queue + 18 * (self.number_in_queue - 1) self.rect_life.left = self.screen_rect.left + 24 else: - # Rendering text using `fg_border`, if there is a lot of time left - self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.fg_color) + # Rendering text using *COLOR_LONG*, if there is a lot of time left + self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.COLOR_LONG) self.rect_life = self.img_life.get_rect() self.rect_life.top = self.screen_rect.top + 2 * self.number_in_queue + 18 * (self.number_in_queue - 1) self.rect_life.left = self.screen_rect.left + 24 @@ -298,7 +353,8 @@ def update(self) -> None: self.kill() def blit(self) -> None: - """ Blit boost """ + """Blit boost + """ if self.is_active: # If boost was activated, blit small and time left images self.screen.blit(self.img_life, self.rect_life) @@ -308,12 +364,15 @@ def blit(self) -> None: self.screen.blit(self.img_idle, self.rect_idle) def activate(self) -> None: - """ Сallback that is called when the boost is activated. Do - not forget to call this if you redefine it in your boost """ + """Сallback that is called when the boost is activated + Importnant: + Do not forget to call this method if you redefine it in your boost + """ # Activate boost self.is_active = True def deactivate(self) -> None: - """ Callback that is called when the boost is deactivated """ + """Callback that is called when the boost is deactivated + """ pass From 0afddf9afd990d2a0d13fef07a878ef25221a272 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 12 Nov 2021 16:24:33 +0300 Subject: [PATCH 17/31] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20mixins.py=20(=D0=B1=D0=B5=D0=B7=20BoostMix?= =?UTF-8?q?in),=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20pygame=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20Python=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 3 + requirements.txt | 2 +- tests/test_mixins.py | 278 ++++++++++++++++++++++++++++++++++++ tests/utils.py | 143 +++++++++++++++++++ 4 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 tests/test_mixins.py create mode 100644 tests/utils.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c8b11a8..20a68c8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -17,6 +17,7 @@ jobs: fail-fast: false matrix: py: + - "3.10" - "3.9" - "3.8" - "3.7" @@ -32,5 +33,7 @@ jobs: run: pip install -r requirements.txt pytest-cov - name: Run tests run: python -m pytest --cov spaceway --cov-report xml --color=yes tests/ + env: + SDL_VIDEODRIVER: dummy - name: Upload coverage uses: codecov/codecov-action@v2 diff --git a/requirements.txt b/requirements.txt index ea6ed93..d2c8b8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pygame==2.0.2.dev2 +pygame==2.0.3 packaging==20.4 requests==2.24.0 platformdirs==2.1.0 \ No newline at end of file diff --git a/tests/test_mixins.py b/tests/test_mixins.py new file mode 100644 index 0000000..2e4e276 --- /dev/null +++ b/tests/test_mixins.py @@ -0,0 +1,278 @@ +from random import randint + +import pytest + +from spaceway.mixins import * +from spaceway.hitbox import Ellipse +from spaceway.collection import SceneButtonsGroup +from utils import * + + +@pytest.mark.parametrize('params,expected', [ + [(46, 88, -4, -29, 30), (30, 30)], + [(121, 290, 20, -30, 290), (-30, 290)], + [(-15, 93, -6, 30, 93), (93, 30)], + [(20, -40, 15, -40, 151), (-40, 151)], + [(32, -48, -5, -48, 86), (86, -48)], + [(85, 10, -8, 10, 90), (90, 10)], + [(-14, -4, -5, -4, 32), (32, -4)], + [(-8, 32, 6, -52, 32), (-52, 32)], + [(30, 56, 7, 5, 60), (5, 60)], + [(-51, -11, -7, -35, 24), (24, -35)] +]) +def test_scene_button_mixin_actions(pygame_env, params, expected): + screen, base_dir, config, clock = pygame_env + x, y, speed, top, bottom = params + + class TestSceneButton(SceneButtonMixin): + def __init__(self, action='stop'): + self.screen = screen + self.img = pygame_surface((randint(20, 120), randint(20, 120))) + self.rect = Ellipse(self.img.get_rect()) + self.rect.topleft = (x, y) + SceneButtonMixin.__init__( + self, base_dir, config, '', '', '', '', + speed, top, bottom, action + ) + + def draw(self): + self.update() + self.blit() + + # Test stop action which passed in `__init__` function + test_button = TestSceneButton() + + @pygame_loop(pygame_env, 0.5) + def loop1(): + test_button.draw() + + assert test_button.rect.topleft == (x, y) + + # Test enter action which passed in `__init__` function + test_button = TestSceneButton('enter') + + @pygame_loop(pygame_env, 1) + def loop2(): + test_button.draw() + + assert test_button.rect.topleft == (x, expected[0]) + + # Test leave action which passed in `__init__` function + test_button = TestSceneButton('leave') + + @pygame_loop(pygame_env, 1) + def loop3(): + test_button.draw() + + assert test_button.rect.topleft == (x, expected[1]) + + # Test enter action which activated via method + test_button = TestSceneButton() + test_button.enter() + + @pygame_loop(pygame_env, 1) + def loop4(): + test_button.draw() + + assert test_button.rect.topleft == (x, expected[0]) + + # Test leave action which activated via method + test_button = TestSceneButton() + test_button.leave() + + @pygame_loop(pygame_env, 1) + def loop5(): + test_button.draw() + + assert test_button.rect.topleft == (x, expected[1]) + + +@pytest.mark.parametrize('params,expected', [ + [(120, 90, -9, 20, 90), 20], + [(-10, 23, 4, 10, 60), 60], + [(30, 42, 0, 42, 42), 42] +]) +def test_scene_button_mixin_scenes(pygame_env, params, expected): + screen, base_dir, config, clock = pygame_env + x, y, speed, top, bottom = params + + scene, sub_scene, change_scene_to, change_sub_scene_to \ + = [rstring() for _ in range(4)] + + class TestSceneButton(SceneButtonMixin): + def __init__(self, pos=(x, y), speed=speed, top=top, bottom=bottom, scene=scene, + sub_scene=sub_scene, change_scene_to=change_scene_to, + change_sub_scene_to=change_sub_scene_to): + self.screen = screen + self.img = pygame_surface((randint(20, 120), randint(20, 120))) + self.rect = Ellipse(self.img.get_rect()) + self.rect.topleft = pos + SceneButtonMixin.__init__( + self, base_dir, config, scene, sub_scene, change_scene_to, + change_sub_scene_to, speed, top, bottom + ) + + def draw(self): + self.update() + self.blit() + + # Test `change_scene` method + test_button = TestSceneButton() + test_button.change_scene() + + assert config['scene'] == change_scene_to and config['sub_scene'] == change_sub_scene_to + + # Test `press` method + config['scene'] = scene + config['sub_scene'] = sub_scene + + test_button = TestSceneButton() + test_button1 = TestSceneButton((0, 0), -5, 0, 50, change_scene_to, change_sub_scene_to) + + _ = SceneButtonsGroup(config, test_button, test_button1) + test_button.press() + + @pygame_loop(pygame_env, 2) + def loop(): + if config['scene'] == change_scene_to and config['sub_scene'] == change_sub_scene_to: + test_button1.draw() + else: + test_button.draw() + + assert config['scene'] == change_scene_to and config['sub_scene'] == change_sub_scene_to + assert test_button.rect.top == expected + assert test_button1.rect.top == 50 + + +@pytest.mark.parametrize('params', [ + ( + rstring(15), + (randint(10, 120), randint(10, 120)) + ) for _ in range(5) +]) +def test_caption_mixin(pygame_env, params): + screen, base_dir, config, clock = pygame_env + caption, topleft = params + + class TestCaption(CaptionMixin): + def __init__(self): + self.screen = screen + CaptionMixin.__init__(self, base_dir, config, caption) + + def draw(self): + self.update() + self.blit() + + # Test first color + config['user']['color'] = 0 + test_caption = TestCaption() + + @pygame_loop(pygame_env, 0.5) + def loop1(): + test_caption.draw() + assert test_caption.rect.topleft == (0, 0) + + colors = most_popular_colors(screen, 2, [(0, 0, 0)]) + assert colors == [(255, 255, 255), (0, 153, 255)] + + # Test second color + config['user']['color'] = 1 + test_caption = TestCaption() + + @pygame_loop(pygame_env, 0.5) + def loop2(): + test_caption.draw() + assert test_caption.rect.topleft == (0, 0) + + colors = most_popular_colors(screen, 2, [(0, 0, 0)]) + assert colors == [(255, 255, 255), (252, 15, 192)] + + # Test third color + config['user']['color'] = 2 + test_caption = TestCaption() + + @pygame_loop(pygame_env, 0.5) + def loop3(): + test_caption.draw() + assert test_caption.rect.topleft == (0, 0) + + colors = most_popular_colors(screen, 2, [(0, 0, 0)]) + assert colors == [(255, 255, 255), (0, 255, 0)] + + # Test `locate` method + def locate(self): + self.rect.topleft = topleft + + TestCaption.locate = locate + test_caption = TestCaption() + + @pygame_loop(pygame_env, 0.5) + def loop4(): + assert test_caption.rect.topleft == topleft + test_caption.draw() + + +@pytest.mark.parametrize('params,expected', [ + [(False, {True: pygame_surface((63, 63)), False: pygame_surface((63, 63), 1)}, None), + (True, False)], + [(True, {True: pygame_surface((42, 20)), False: pygame_surface((42, 20), 1)}, None), + (False, True)], + [( + 0, {0: pygame_surface((58, 32)), 1: pygame_surface((58, 32), 1), 2: pygame_surface((58, 32), 2)}, + lambda self: setattr(self, 'state', (self.state + 1) % 3) + ), (1, 2, 0)], + [( + 'a', {'a': pygame_surface((41, 71)), 'aa': pygame_surface((41, 71), 1), 'aaa': pygame_surface((41, 71), 2)}, + lambda self: setattr(self, 'state', 'a' if len(self.state) == 3 else self.state + 'a') + ), ('aa', 'aaa', 'a')], + [( + 2, {0: pygame_surface((60, 55)), 1: pygame_surface((60, 55), 1), + 2: pygame_surface((60, 55), 2), 3: pygame_surface((60, 55), 1)}, + lambda self: setattr(self, 'state', (self.state - 1) % 4) + ), (1, 0, 3, 2)] +]) +def test_setttings_button_mixin(pygame_env, params, expected): + screen, base_dir, config, clock = pygame_env + config_value, imgs, change_state = params + config_index = rstring(15) + + class TestSettingsButton(SettingsButtonMixin): + def __init__(self): + self.imgs = imgs + SettingsButtonMixin.__init__(self, screen, config, config_index) + + def draw(self): + self.update() + self.blit() + + # Test `update` and `blit` methods + config['user'][config_index] = config_value + + test_button = TestSettingsButton() + assert test_button.state == config_value + assert test_button.img == test_button.imgs[config_value] + + @pygame_loop(pygame_env, 0.5) + def loop1(): + test_button.draw() + + # Test `change_state` with `update` method + if change_state: + TestSettingsButton.change_state = change_state + + test_button = TestSettingsButton() + + for state in expected: + test_button.change_state() + assert test_button.state == state + + test_button.update() + assert test_button.img == test_button.imgs[state] + + # Test `press` with method + test_button = TestSettingsButton() + + for state in expected: + test_button.press() + assert test_button.state == state + assert test_button.img == test_button.imgs[state] diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..50c6e48 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,143 @@ +import os +from time import time +from random import choices +from string import ascii_letters + +import pytest + +import pygame +import spaceway + + +@pytest.fixture(scope='module') +def pygame_env(): + """Creates the basic environment of the game + + Returns: + screen (pygame.Surface): Screen (surface) obtained via pygame + base_dir (str): An absolute path to directory where file with the main + entrypoint is located + config (spaceway.config.ConfigManager): The configuration object + clock (pygame.time.Clock): Clock object obtained via pygame + """ + base_dir = os.path.dirname(os.path.abspath(spaceway.main.__file__)) + config = spaceway.config.ConfigManager(base_dir) + screen = pygame.display.set_mode(config['mode']) + + config['ns'].dt = 0 + config['ns'].tick = 1 + config['ns'].speed = 2 + + clock = pygame.time.Clock() + + return screen, base_dir, config, clock + + +def pygame_loop(pygame_env, duration): + """Creates a decorator for quickly creating a game loop + + Args: + pygame_env (tuple): The environment of the game created with :pygame_env:`tests.pygame_env` + duration (float): Loop duration (in seconds) + + Returns: + callable: Customized decorator + + Example: + .. code:: python + + @pygame_loop(pygame_env, 5) + def test_loop(): + button.update() + ... + button.blit() + ... + print('Go to another iteration!') + + >>> Go to another iteration! + >>> Go to another iteration! + ... + >>> Go to another iteration! + >>> + """ + def decorator(func): + screen, base_dir, config, clock = pygame_env + config['ns'].dt = 1000 / config['FPS'] * 0.03 + end = time() + duration + + while end > time(): + pygame.event.get() + screen.fill((0, 0, 0)) + + func() + + pygame.display.update() + config['dt'] = clock.tick(config['FPS']) * 0.03 + config['ns'].tick += 1 + + return decorator + + +def pygame_surface(size, color=0): + """Creates a colored test surface + + Args: + size (Tuple[int, int]): Size (width and height) of surface + color (int): Index of specific color, defaults to 0 + + Returns: + pygame.Surface: Colored surface (grid 2x2 colored in two colors, in a staggered order) + """ + COLORS = ( + ((0, 57, 255), (255, 46, 222)), + ((0, 195, 12), (251, 255, 0)), + ((255, 11, 0), (0, 255, 228)) + ) + + s = pygame.Surface(size) + r = s.get_rect() + + a, b = r.w // 2, r.h // 2 + + pygame.draw.rect(s, COLORS[color][0], pygame.Rect(0, 0, a, b)) + pygame.draw.rect(s, COLORS[color][1], pygame.Rect(a, 0, a, b)) + pygame.draw.rect(s, COLORS[color][1], pygame.Rect(0, b, a, b)) + pygame.draw.rect(s, COLORS[color][0], pygame.Rect(a, b, a, b)) + + return s + + +def most_popular_colors(surface, amount=1, exclude=[]): + """Finds the most common surface colors + + Args: + surface (pygame.Surface): Surface on which the colors will be searched + amount (int): Amount of returned colors, defaults to 0 + exclude (list): List of colors that don't need to be counted, defaluts to empty list + + Returns: + List[Tuple[int, int, int]]: List of the most common colors + + Important: + Colors are considered without taking into account the alpha channel, i.e. the function + considers rgba(1, 12, 123, 55) and rgb(1, 12, 123) to be the same, while fully transparent + pixels (alpha = 0) aren't taken into account in the calculations + """ + width, height = surface.get_size() + colors = {} + + for x in range(0, width): + for y in range(0, height): + pixel = surface.get_at((x, y)) + + if len(pixel) == 3 and pixel not in exclude: + colors[pixel] = colors.get(pixel, 0) + elif (len(pixel) == 4 and pixel[3] != 0) and pixel[:3] not in exclude: + colors[pixel[:3]] = colors.get(pixel[:3], 0) + 1 + + sorted_colors = sorted(colors, key=lambda x: colors[x], reverse=True) + return sorted_colors[:amount] + + +def rstring(k=5): + return ''.join(choices(ascii_letters, k=k)) From 0fcf2e4c702ecaaaaf4cd222e6ede0955f56ef67 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 13 Nov 2021 18:44:50 +0300 Subject: [PATCH 18/31] =?UTF-8?q?=D0=9F=D0=BE=D0=B2=D1=8B=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=D0=B8=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D0=B0=20hitbox.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/hitbox.py | 5 +- tests/test_hitbox.py | 132 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/spaceway/hitbox.py b/spaceway/hitbox.py index 5f11071..db0616b 100644 --- a/spaceway/hitbox.py +++ b/spaceway/hitbox.py @@ -176,6 +176,9 @@ def __eq__(self, other): def __bool__(self): return self._rect[2] != 0 and self._rect[3] != 0 + def __hash__(self): + return hash(str(self)) + def copy(self): return self.__class__(self._rect) @@ -240,7 +243,7 @@ def clip(self, arg): raise TypeError("Argument must be hitbox style object") if isinstance(arg, Ellipse): - return self.__clip_ellipse(Ellpse(arg)) + return self._clip_ellipse(Ellipse(arg)) return self._clip_rect(Rect(arg)) def _clip_rect(self, rect): diff --git a/tests/test_hitbox.py b/tests/test_hitbox.py index 3806d03..23fb6bd 100644 --- a/tests/test_hitbox.py +++ b/tests/test_hitbox.py @@ -1,41 +1,110 @@ import pytest from random import randint +from re import match -from spaceway.hitbox import Rect, Ellipse +from spaceway.hitbox import Hitbox, Rect, Ellipse from pygame import Rect as PgRect +from utils import rstring + + +def test_hitbox_init(): + assert ( + Hitbox(100, 123, 10, 29) == Hitbox((100, 123), (10, 29)) == + Hitbox((100, 123, 10, 29)) == Hitbox(((100, 123), (10, 29))) == + Hitbox(Hitbox(100, 123, 10, 29)) == Hitbox(Rect(100, 123, 10, 29)) == + Hitbox(Ellipse(100, 123, 10, 29)) == Hitbox(PgRect(100, 123, 10, 29)) == + PgRect(Hitbox(100, 123, 10, 29)) + ) + assert ( + Hitbox(-100, 2, 0, -2) == Hitbox((-100, 2), (0, -2)) == + Hitbox((-100, 2, 0, -2)) == Hitbox(((-100, 2), (0, -2))) == + Hitbox(Hitbox(-100, 2, 0, -2)) == Hitbox(Rect(-100, 2, 0, -2)) == + Hitbox(Ellipse(-100, 2, 0, -2)) == Hitbox(PgRect(-100, 2, 0, -2)) == + PgRect(Hitbox(-100, 2, 0, -2)) + ) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Hitbox((100, 123), (10,)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Hitbox((100,), (10,)) + + with pytest.raises(TypeError, match=r'sequence argument takes 2 or 4 items \(\d given\)'): + Hitbox((100, 123, 10)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Hitbox(100, 123, 10) + def test_rect_init(): assert ( Rect(100, 123, 10, 29) == Rect((100, 123), (10, 29)) == - Rect(Rect(100, 123, 10, 29)) == Rect(Ellipse(100, 123, 10, 29)) == - Rect(PgRect(100, 123, 10, 29)) == PgRect(Rect(100, 123, 10, 29)) + Rect((100, 123, 10, 29)) == Rect(((100, 123), (10, 29))) == + Rect(Hitbox(100, 123, 10, 29)) == Rect(Rect(100, 123, 10, 29)) == + Rect(Ellipse(100, 123, 10, 29)) == Rect(PgRect(100, 123, 10, 29)) == + PgRect(Rect(100, 123, 10, 29)) ) assert ( Rect(-100, 2, 0, -2) == Rect((-100, 2), (0, -2)) == - Rect(Rect(-100, 2, 0, -2)) == Rect(Ellipse(-100, 2, 0, -2)) == - Rect(PgRect(-100, 2, 0, -2)) == PgRect(Rect(-100, 2, 0, -2)) + Rect((-100, 2, 0, -2)) == Rect(((-100, 2), (0, -2))) == + Rect(Hitbox(-100, 2, 0, -2)) == Rect(Rect(-100, 2, 0, -2)) == + Rect(Ellipse(-100, 2, 0, -2)) == Rect(PgRect(-100, 2, 0, -2)) == + PgRect(Rect(-100, 2, 0, -2)) ) + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Rect((100, 123), (10,)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Rect((100,), (10,)) + + with pytest.raises(TypeError, match=r'sequence argument takes 2 or 4 items \(\d given\)'): + Rect((100, 123, 10)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Rect(100, 123, 10) + def test_ellipse_init(): assert ( Ellipse(100, 123, 10, 29) == Ellipse((100, 123), (10, 29)) == - Ellipse(Ellipse(100, 123, 10, 29)) == Ellipse(Rect(100, 123, 10, 29)) == - Ellipse(PgRect(100, 123, 10, 29)) == PgRect(Ellipse(100, 123, 10, 29)) + Ellipse((100, 123, 10, 29)) == Ellipse(((100, 123), (10, 29))) == + Ellipse(Hitbox(100, 123, 10, 29)) == Ellipse(Rect(100, 123, 10, 29)) == + Ellipse(Ellipse(100, 123, 10, 29)) == Ellipse(PgRect(100, 123, 10, 29)) == + PgRect(Ellipse(100, 123, 10, 29)) ) assert ( Ellipse(-100, 2, 0, -2) == Ellipse((-100, 2), (0, -2)) == - Ellipse(Ellipse(-100, 2, 0, -2)) == Ellipse(Rect(-100, 2, 0, -2)) == - Ellipse(PgRect(-100, 2, 0, -2)) == PgRect(Ellipse(-100, 2, 0, -2)) + Ellipse((-100, 2, 0, -2)) == Ellipse(((-100, 2), (0, -2))) == + Ellipse(Hitbox(-100, 2, 0, -2)) == Ellipse(Rect(-100, 2, 0, -2)) == + Ellipse(Ellipse(-100, 2, 0, -2)) == Ellipse(PgRect(-100, 2, 0, -2)) == + PgRect(Ellipse(-100, 2, 0, -2)) ) + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Ellipse((100, 123), (10,)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Ellipse((100,), (10,)) + + with pytest.raises(TypeError, match=r'sequence argument takes 2 or 4 items \(\d given\)'): + Ellipse((100, 123, 10)) + + with pytest.raises(TypeError, match='Argument must be hitbox style object'): + Ellipse(100, 123, 10) + @pytest.mark.parametrize('hitbox', [ + Hitbox(100, 123, 10, 29), + Hitbox(-100, 2, 0, -2), + Hitbox(41, -32, -25, -1), Rect(100, 123, 10, 29), Rect(-100, 2, 0, -2), + Rect(41, -32, -25, -1), Ellipse(100, 123, 10, 29), - Ellipse(-100, 2, 0, -2) + Ellipse(-100, 2, 0, -2), + Ellipse(41, -32, -25, -1), ]) def test_generic(hitbox): # Test `copy` method @@ -51,7 +120,12 @@ def test_generic(hitbox): ) assert len(hitbox) == len(PgRect(hitbox)) assert bool(hitbox) == bool(PgRect(hitbox)) + assert (hitbox == rstring()) is False + p = r'^<\w+\(((-?\d+|-?\d+\.\d+), ){3}(-?\d+|-?\d+\.\d+)\)>$' + assert match(p, str(hitbox)) and match(p, repr(hitbox)) + + assert hash(hitbox) == hash(str(hitbox)) # Test `getattr_dict` and methods which operates with it hitbox_copy = hitbox.copy() @@ -95,13 +169,42 @@ def test_generic(hitbox): hitbox_copy.normalize() pgrect.normalize() - assert hitbox_copy == pgrect + assert hitbox_copy.trunc() == pgrect # Test `update` hitbox_copy.update(hitbox) assert hitbox_copy == hitbox +@pytest.mark.parametrize('arg,exception', [ + (Ellipse(150, 70, 73, 130), NotImplementedError), + (Ellipse(198, 190, 65, 20), NotImplementedError), + (Rect(100, 123, 10, 29), NotImplementedError), + (Rect(-100, 2, 40, -2), NotImplementedError), + (randint(1, 100), TypeError), + (rstring(), TypeError) +]) +def test_exceptions(arg, exception): + hitbox = Hitbox(100, 123, 10, 29) + + methods_with_arg = ('clamp', 'clip', 'union', 'fit', 'contains', 'colliderect') + methods_with_args = ('unionall', 'collidelist', 'collidelistall') + + for method in methods_with_arg: + with pytest.raises(exception): + getattr(hitbox, method)(arg) + + for method in methods_with_args: + with pytest.raises(exception): + getattr(hitbox, method)([arg]) + + with pytest.raises(TypeError): + hitbox.collidepoint(1, 2, 3) + + with pytest.raises(NotImplementedError): + hitbox.collidepoint(1, 2) + + @pytest.mark.parametrize('rect', [ Rect(23, 83, 40, 10), Rect(100, 83, 120, 251), @@ -124,17 +227,24 @@ def test_rect_with_rect(rect, arg): # Test `unionall (_ip)`, `collidelist` and `collidelistall` assert rect.unionall([arg, rect]) == PgRect(rect).unionall([arg, rect]) assert rect.collidelist([arg, rect, arg, rect]) == PgRect(rect).collidelist([arg, rect, arg, rect]) + assert rect.collidelist([]) == PgRect(rect).collidelist([]) assert rect.collidelistall([arg, rect, arg, rect]) == PgRect(rect).collidelistall([arg, rect, arg, rect]) + assert rect.collidelistall([]) == PgRect(rect).collidelistall([]) # Test `collidedict` and `collidedictall` assert ( rect.collidedict({0: arg, 1: rect, True: arg, None: rect}, True) == PgRect(rect).collidedict({0: arg, 1: rect, True: arg, None: rect}, True) ) + assert rect.collidedict({rect: 0}) == PgRect(rect).collidedict({rect: 0}) + assert rect.collidedict({}) == PgRect(rect).collidedict({}) + assert ( rect.collidedictall({0: arg, 1: rect, True: arg, None: rect}, True) == PgRect(rect).collidedictall({0: arg, 1: rect, True: arg, None: rect}, True) ) + assert rect.collidedictall({rect: 0}) == PgRect(rect).collidedictall({rect: 0}) + assert rect.collidedictall({}) == PgRect(rect).collidedictall({}) # Test `collidepoint` point1 = (randint(-20, 20), randint(-20, 20)) From 6ae4fdbdcbfe8a22edeb6f6d89685b4164138b74 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 13 Nov 2021 20:03:38 +0300 Subject: [PATCH 19/31] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D1=81=D1=87=D0=B5=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=20pause=20=D0=BF=D0=BE=D0=B4=D1=81=D1=86=D0=B5=D0=BD=D1=8B=20(?= =?UTF-8?q?#13),=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=8D=D1=82=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BB=D1=83=D1=87=D0=B0?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/collection.py | 4 ---- spaceway/mixins.py | 4 ++-- spaceway/scenes/game/functions.py | 2 ++ tests/test_mixins.py | 4 +++- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spaceway/collection.py b/spaceway/collection.py index 74ec0d9..1d7b47a 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -298,10 +298,6 @@ def perform_point_collides(self, point): for button in self.get_by_scene(): # If collision was found if button.rect.collidepoint(point): - # Leave buttons of current scene and enter buttons of next scene - self.leave_buttons(self.config['scene'], self.config['sub_scene']) - self.enter_buttons(button.change_scene_to, button.change_sub_scene_to) - # Press collided button button.press() diff --git a/spaceway/mixins.py b/spaceway/mixins.py index 15b6e14..f8e394d 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -36,7 +36,7 @@ class SceneButtonMixin(pygame.sprite.Sprite): """ def __init__(self, base_dir, config, scene, sub_scene, change_scene_to, - change_sub_scene_to, speed=0, top=inf, bottom=inf, + change_sub_scene_to, speed=0, top=-inf, bottom=inf, action='stop'): """Constructor method """ @@ -68,7 +68,7 @@ def update(self) -> None: if self.action != 'stop': # Check, if move can be continued inc = (self.speed if self.action == 'leave' else -self.speed) * self.config['ns'].dt - if self.top < self.rect.y + inc < self.bottom: + if self.speed and self.top < self.rect.y + inc < self.bottom: # If can be, move button self.rect.y += inc else: diff --git a/spaceway/scenes/game/functions.py b/spaceway/scenes/game/functions.py index 250fe64..5a73b2a 100644 --- a/spaceway/scenes/game/functions.py +++ b/spaceway/scenes/game/functions.py @@ -13,7 +13,9 @@ def check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_butto exit() if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + scene_buttons.leave_buttons() config['sub_scene'] = 'pause' + scene_buttons.enter_buttons() elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: if plate.rect.top >= plate.screen_rect.top + 50 and not plate.flip: diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 2e4e276..294ef21 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -1,4 +1,5 @@ from random import randint +from math import inf import pytest @@ -18,7 +19,8 @@ [(-14, -4, -5, -4, 32), (32, -4)], [(-8, 32, 6, -52, 32), (-52, 32)], [(30, 56, 7, 5, 60), (5, 60)], - [(-51, -11, -7, -35, 24), (24, -35)] + [(-51, -11, -7, -35, 24), (24, -35)], + [(13, 118, 0, -inf, inf), (118, 118)] ]) def test_scene_button_mixin_actions(pygame_env, params, expected): screen, base_dir, config, clock = pygame_env From 021f07c18d671372e9a3dad6d1bc5abd3e8f302b Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sun, 14 Nov 2021 17:49:27 +0300 Subject: [PATCH 20/31] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BC=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=B4=D1=81=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=20game=20=D0=BD=D0=B0=20pause=20?= =?UTF-8?q?=D0=B8=20=D1=81=20pause=20=D0=BD=D0=B0=20lobby,=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BD=D0=B0=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=83=20=D1=81=D1=86=D0=B5=D0=BD=20=D0=B1=D0=B0?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D1=8B=D1=85=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20(check=5Fevents=20=D0=B8=20update)=20=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20ini?= =?UTF-8?q?t=20=D1=83=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=B9=20=D1=81?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +-- spaceway/main.py | 4 ++-- spaceway/scenes/game/__init__.py | 10 +++++----- spaceway/scenes/game/functions.py | 15 +++++++++++---- spaceway/scenes/game/objects.py | 30 ++++++++++++++++-------------- tests/test_scenes.py | 19 +++++++++++++++++++ 6 files changed, 54 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 5e282ed..332b1f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ venv __pycache__ *.pyc -*.debug.* .p4a other tmp build dist *.egg-info -aliases +.coverage diff --git a/spaceway/main.py b/spaceway/main.py index f572ba2..a7cdc78 100644 --- a/spaceway/main.py +++ b/spaceway/main.py @@ -71,7 +71,7 @@ def main() -> None: astrs = pygame.sprite.Group() boosts = collection.BoostsGroup() - bg, plate, score, end, pause, resume_button, pause_lobby_button, again_button, end_lobby_button = scenes.game.init(screen, base_dir, config, astrs, boosts) + bg, plate, score, end, pause, resume_button, pause_lobby_button, again_button, end_lobby_button, pause_button = scenes.game.init(screen, base_dir, config) pause_buttons = collection.CenteredButtonsGroup(config['mode']) pause_buttons.add(pause_lobby_button, resume_button) @@ -83,7 +83,7 @@ def main() -> None: scene_buttons = collection.SceneButtonsGroup(config) scene_buttons.add(play_button, table_button, settings_button, settings_back_button, table_back_button, resume_button, - pause_lobby_button, again_button, end_lobby_button) + pause_lobby_button, again_button, end_lobby_button, pause_button) while True: # Showing a specific scene diff --git a/spaceway/scenes/game/__init__.py b/spaceway/scenes/game/__init__.py index 36f8157..cf9882e 100644 --- a/spaceway/scenes/game/__init__.py +++ b/spaceway/scenes/game/__init__.py @@ -1,8 +1,7 @@ from .objects import * -from .functions import defeat -def init(screen, base_dir, config, astrs, boosts): +def init(screen, base_dir, config): config['ns'].speed = 2 config['ns'].score = 0 @@ -13,11 +12,12 @@ def init(screen, base_dir, config, astrs, boosts): pause = PauseCaption(screen, base_dir, config) resume_button = ResumeButton(screen, base_dir, config) - pause_lobby_button = PauseLobbyButton(screen, base_dir, config, defeat, - plate, astrs, boosts, end, config, base_dir) + pause_lobby_button = PauseLobbyButton(screen, base_dir, config) again_button = AgainButton(screen, base_dir, config) end_lobby_button = EndLobbyButton(screen, base_dir, config) + pause_button = PauseButton(screen, base_dir, config) + return bg, plate, score, end, pause, resume_button, \ - pause_lobby_button, again_button, end_lobby_button + pause_lobby_button, again_button, end_lobby_button, pause_button diff --git a/spaceway/scenes/game/functions.py b/spaceway/scenes/game/functions.py index 5a73b2a..65c8053 100644 --- a/spaceway/scenes/game/functions.py +++ b/spaceway/scenes/game/functions.py @@ -13,9 +13,7 @@ def check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_butto exit() if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: - scene_buttons.leave_buttons() - config['sub_scene'] = 'pause' - scene_buttons.enter_buttons() + scene_buttons.get_by_instance(PauseButton).press() elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: if plate.rect.top >= plate.screen_rect.top + 50 and not plate.flip: @@ -61,7 +59,14 @@ def check_events(config, base_dir, plate, astrs, boosts, end, pause, scene_butto elif event.type == pygame.MOUSEBUTTONDOWN: x, y = pygame.mouse.get_pos() - scene_buttons.perform_point_collides((x, y)) + pause_lobby_button = scene_buttons.get_by_instance(PauseLobbyButton) + if pause_lobby_button.rect.collidepoint((x, y)): + defeat(plate, astrs, boosts, end, config, base_dir) + config['scene'] = 'game' + config['sub_scene'] = 'pause' + pause_lobby_button.press() + else: + scene_buttons.perform_point_collides((x, y)) def spawn(screen, base_dir, config, plate, astrs, boosts): @@ -112,6 +117,8 @@ def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause bg.update() bg.blit() + scene_buttons.draw() + if config['ns'].tick % (config['FPS'] * 7) == 0: if 'time' in boosts: boosts.get('time').speed += 1 diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index 2a943ab..8d4ce2a 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -314,27 +314,18 @@ def __init__(self, screen, base_dir, config): self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/resume.bmp') self.rect = Ellipse(self.img.get_rect()) - SceneButtonMixin.__init__(self, base_dir, config, 'game', 'pause', 'game', 'game', 0) + SceneButtonMixin.__init__(self, base_dir, config, 'game', 'pause', 'game', 'game') class PauseLobbyButton(SceneButtonMixin): - def __init__(self, screen, base_dir, config, defeat, *defeat_args): + def __init__(self, screen, base_dir, config): self.screen = screen self.screen_rect = self.screen.get_rect() self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') self.rect = Ellipse(self.img.get_rect()) - self.defeat = defeat - self.defeat_args = defeat_args - - SceneButtonMixin.__init__(self, base_dir, config, 'game', 'pause', 'lobby', 'lobby', 0) - - def press(self): - self.defeat(*self.defeat_args) - self.config['scene'] = 'game' - self.config['sub_scene'] = 'pause' - self.leave(self.change_scene) + SceneButtonMixin.__init__(self, base_dir, config, 'game', 'pause', 'lobby', 'lobby') class AgainButton(SceneButtonMixin): @@ -345,7 +336,7 @@ def __init__(self, screen, base_dir, config): self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/again.bmp') self.rect = Ellipse(self.img.get_rect()) - SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'game', 'game', 0) + SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'game', 'game') class EndLobbyButton(SceneButtonMixin): @@ -356,4 +347,15 @@ def __init__(self, screen, base_dir, config): self.img = pygame.image.load(f'{base_dir}/assets/images/buttons/lobby.bmp') self.rect = Ellipse(self.img.get_rect()) - SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'lobby', 'lobby', 0) + SceneButtonMixin.__init__(self, base_dir, config, 'game', 'end', 'lobby', 'lobby') + + +class PauseButton(SceneButtonMixin): + def __init__(self, screen, base_dir, config): + self.screen = screen + self.screen_rect = self.screen.get_rect() + + self.img = pygame.Surface((0, 0)) + self.rect = self.img.get_rect() + + SceneButtonMixin.__init__(self, base_dir, config, 'game', 'game', 'game', 'pause') diff --git a/tests/test_scenes.py b/tests/test_scenes.py index a5abdf2..4635c08 100644 --- a/tests/test_scenes.py +++ b/tests/test_scenes.py @@ -1,7 +1,10 @@ import os +from importlib import import_module import spaceway +from utils import pygame_env + ROOT_DIR = os.path.dirname(os.path.dirname(__file__)) + '/' @@ -21,3 +24,19 @@ def test_recursive_import(): for imp in files: obj = root.replace(ROOT_DIR, '').replace('/', '.') + '.' + imp[:-3] assert obj in dir(spaceway.scenes) + + +def test_scenes_functions_availability(pygame_env): + exclude_dirs = ('__pycache__',) + functions = ('check_events', 'update') + + for root, dirs, files in os.walk(os.path.dirname(spaceway.scenes.__file__)): + dirs[:] = [d for d in dirs if d not in exclude_dirs] + + if not dirs: + scene = import_module(root.replace(ROOT_DIR, '').replace('/', '.')) + + for function in functions: + getattr(scene.functions, function) + + scene.init(*pygame_env[:-1]) From 0d7fe8023a1f2aaee9eb0f6d0376a2b1746939fe Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sun, 14 Nov 2021 19:02:51 +0300 Subject: [PATCH 21/31] =?UTF-8?q?Revert=20"=D0=A1=D0=B6=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=B5=20SVG=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 532b495c32869ebb04a977e4071417e91f57d098. --- docs/icons/icon.svg | 318 +++++++++++++++++++++++++++++++++++++++- docs/icons/icon_fit.svg | 314 ++++++++++++++++++++++++++++++++++++++- setupfiles/spaceway.svg | 318 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 947 insertions(+), 3 deletions(-) diff --git a/docs/icons/icon.svg b/docs/icons/icon.svg index 1687b8d..7a67a3c 100644 --- a/docs/icons/icon.svg +++ b/docs/icons/icon.svg @@ -1 +1,317 @@ - \ No newline at end of file + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/icons/icon_fit.svg b/docs/icons/icon_fit.svg index 4fe82cf..5c5ef33 100644 --- a/docs/icons/icon_fit.svg +++ b/docs/icons/icon_fit.svg @@ -1 +1,313 @@ - \ No newline at end of file + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setupfiles/spaceway.svg b/setupfiles/spaceway.svg index 1687b8d..7a67a3c 100644 --- a/setupfiles/spaceway.svg +++ b/setupfiles/spaceway.svg @@ -1 +1,317 @@ - \ No newline at end of file + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f35137d102749b4a7ac925fe454560eafce9d5bf Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Wed, 17 Nov 2021 20:54:55 +0300 Subject: [PATCH 22/31] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D1=87=D0=B5=D1=82=D0=B0=20=D0=B6=D0=B8?= =?UTF-8?q?=D0=B7=D0=BD=D0=B8=20BoostMixin,=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20BoostMixin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/mixins.py | 30 ++++++------ spaceway/scenes/game/objects.py | 16 ++----- tests/test_mixins.py | 85 +++++++++++++++++++++++++++++++++ tests/utils.py | 1 + 4 files changed, 106 insertions(+), 26 deletions(-) diff --git a/spaceway/mixins.py b/spaceway/mixins.py index f8e394d..b87f825 100644 --- a/spaceway/mixins.py +++ b/spaceway/mixins.py @@ -2,7 +2,7 @@ objects and following the DRY principle """ from random import randint -from math import inf +from math import inf, ceil import pygame @@ -269,7 +269,7 @@ def press(self) -> None: self.update() -class BoostMixin: +class BoostMixin(pygame.sprite.Sprite): """Mixin for creating boosts. Must be initialized at the bottom of your `__init__` function @@ -293,6 +293,8 @@ class BoostMixin: def __init__(self, screen, base_dir, config, name, life): """Constructor method """ + pygame.sprite.Sprite.__init__(self) + # Setting variables for the further use self.screen = screen self.screen_rect = self.screen.get_rect() @@ -303,7 +305,6 @@ def __init__(self, screen, base_dir, config, name, life): self.name = name self.life = life self.is_active = False - self.tick = 0 # Generating a hitbox of :img_idle:`pygame.Surface` and randomly positioning it self.rect_idle = Ellipse(self.img_idle.get_rect()) @@ -320,30 +321,31 @@ def update(self) -> None: """ # If boost was activated if self.is_active: + # Count life time + self.life -= self.config['ns'].dt / 30 + + if self.life <= 0: + # Deactivate and kill the boost if there is no time left + self.deactivate() + self.kill() + return + # Vertical positioning of boost, taking into account the number in the boost queue self.rect_small.top = self.screen_rect.top + 2 * self.number_in_queue + 18 * (self.number_in_queue - 1) # Generating text with the remaining lifetime - if (self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1 <= 3: + if ceil(self.life) <= 3: # Rendering text using *COLOR_SHORT*, if there is little time left - self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.COLOR_SHORT) + self.img_life = self.font.render(f"{ceil(self.life)}S", True, self.COLOR_SHORT) self.rect_life = self.img_life.get_rect() self.rect_life.top = self.screen_rect.top + 2 * self.number_in_queue + 18 * (self.number_in_queue - 1) self.rect_life.left = self.screen_rect.left + 24 else: # Rendering text using *COLOR_LONG*, if there is a lot of time left - self.img_life = self.font.render(f"{(self.life * self.config['FPS'] - self.tick) // self.config['FPS'] + 1}S", True, self.COLOR_LONG) + self.img_life = self.font.render(f"{ceil(self.life)}S", True, self.COLOR_LONG) self.rect_life = self.img_life.get_rect() self.rect_life.top = self.screen_rect.top + 2 * self.number_in_queue + 18 * (self.number_in_queue - 1) self.rect_life.left = self.screen_rect.left + 24 - - if self.life * self.config['FPS'] - self.tick <= 0: - # Deactivate and kill the boost if there is no time left - self.deactivate() - self.kill() - else: - # Continue count life time if there is a lot of time left - self.tick += 1 else: # Continue movement of boost if it has not activated yet self.rect_idle.x -= self.config['ns'].speed * self.config['ns'].dt diff --git a/spaceway/scenes/game/objects.py b/spaceway/scenes/game/objects.py index 8d4ce2a..5ad2d78 100644 --- a/spaceway/scenes/game/objects.py +++ b/spaceway/scenes/game/objects.py @@ -174,10 +174,8 @@ def update(self): self.rect.bottomleft = self.rect_blit.bottomleft -class TimeBoost(BoostMixin, pygame.sprite.Sprite): +class TimeBoost(BoostMixin): def __init__(self, screen, base_dir, config, life=5): - pygame.sprite.Sprite.__init__(self) - self.speed = 2 self.img_idle = pygame.image.load(f'{base_dir}/assets/images/boosts/time_idle.bmp') @@ -194,20 +192,16 @@ def deactivate(self): self.config['ns'].speed = self.speed -class DoubleBoost(BoostMixin, pygame.sprite.Sprite): +class DoubleBoost(BoostMixin): def __init__(self, screen, base_dir, config, life=5): - pygame.sprite.Sprite.__init__(self) - self.img_idle = pygame.image.load(f'{base_dir}/assets/images/boosts/double_idle.bmp') self.img_small = pygame.image.load(f'{base_dir}/assets/images/boosts/double_small.bmp') BoostMixin.__init__(self, screen, base_dir, config, 'double', life) -class ShieldBoost(BoostMixin, pygame.sprite.Sprite): +class ShieldBoost(BoostMixin): def __init__(self, screen, base_dir, config, plate, life=5): - pygame.sprite.Sprite.__init__(self) - self.plate = plate self.img_idle = pygame.image.load(f'{base_dir}/assets/images/boosts/shield_idle.bmp') @@ -231,10 +225,8 @@ def blit(self): self.screen.blit(self.img_active, self.rect_active) -class MirrorBoost(BoostMixin, pygame.sprite.Sprite): +class MirrorBoost(BoostMixin): def __init__(self, screen, base_dir, config, plate, life=5): - pygame.sprite.Sprite.__init__(self) - self.plate = plate self.img_idle = pygame.image.load(f'{base_dir}/assets/images/boosts/mirror_idle.bmp') diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 294ef21..41f5258 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -1,7 +1,9 @@ from random import randint from math import inf +from time import time import pytest +import pygame from spaceway.mixins import * from spaceway.hitbox import Ellipse @@ -278,3 +280,86 @@ def loop1(): test_button.press() assert test_button.state == state assert test_button.img == test_button.imgs[state] + + +@pytest.mark.parametrize('life', [ + 4, 2, 6 +]) +def test_boost_mixin(pygame_env, life): + screen, base_dir, config, clock = pygame_env + + class TestBoost(BoostMixin, pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self) + + self.img_idle = pygame_surface((30, 30)) + self.img_small = pygame_surface((18, 18), 1) + self.number_in_queue = randint(1, 4) + + BoostMixin.__init__(self, screen, base_dir, config, rstring(), life) + + def draw(self): + self.update() + self.blit() + + config['ns'].speed = 18 + + # Test boost alive and position + test_boost = TestBoost() + y = test_boost.rect.y + + assert test_boost.rect.x == screen.get_width() + assert 0 <= y <= screen.get_height() - test_boost.rect.h - 2 + assert test_boost.rect_small.topleft == (2, 0) + + _ = pygame.sprite.Group(test_boost) + + @pygame_loop(pygame_env, 2) + def loop1(): + test_boost.draw() + + assert test_boost.rect.right < 0 + assert test_boost.rect.y == y + assert not test_boost.alive() + + # Test boost life time after activation + test_boost = TestBoost() + test_boost.activate() + + _ = pygame.sprite.Group(test_boost) + end = int(time() + life) + + @pygame_loop(pygame_env, life + 1) + def loop2(): + test_boost.draw() + + if int(time()) < end: + assert test_boost.alive() + + assert not test_boost.alive() + + # Test boost color of life time + test_boost = TestBoost() + test_boost.activate() + + end = int(time() + life - 3) + + @pygame_loop(pygame_env, life + 1) + def loop3(): + test_boost.draw() + + if int(time()) < end: + assert most_popular_colors(test_boost.img_life, 1, [(0, 0, 0)])[0] == BoostMixin.COLOR_LONG + + assert most_popular_colors(test_boost.img_life, 1, [(0, 0, 0)])[0] == BoostMixin.COLOR_SHORT + + # Test custom `deactivate` method + TestBoost.deactivate = lambda self: 1 / 0 + + test_boost = TestBoost() + test_boost.activate() + + with pytest.raises(ZeroDivisionError): + @pygame_loop(pygame_env, life + 1) + def loop4(): + test_boost.draw() diff --git a/tests/utils.py b/tests/utils.py index 50c6e48..bcd913e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,6 +27,7 @@ def pygame_env(): config['ns'].dt = 0 config['ns'].tick = 1 config['ns'].speed = 2 + config['ns'].score = 0 clock = pygame.time.Clock() From 4b643244df884674c3866f0a1c1cc49047ab4b46 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Wed, 17 Nov 2021 21:30:57 +0300 Subject: [PATCH 23/31] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20CI=20(=D0=BD=D0=B5=D0=BA=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20test=5Fmixins.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_mixins.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 41f5258..cd001ee 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -327,30 +327,27 @@ def loop1(): test_boost.activate() _ = pygame.sprite.Group(test_boost) - end = int(time() + life) + assert test_boost.alive() - @pygame_loop(pygame_env, life + 1) + @pygame_loop(pygame_env, life) def loop2(): test_boost.draw() - if int(time()) < end: - assert test_boost.alive() - assert not test_boost.alive() # Test boost color of life time test_boost = TestBoost() test_boost.activate() + test_boost.update() + assert ( + most_popular_colors(test_boost.img_life, 1, [(0, 0, 0)])[0] == + (BoostMixin.COLOR_LONG if life > 2 else BoostMixin.COLOR_SHORT) + ) - end = int(time() + life - 3) - - @pygame_loop(pygame_env, life + 1) + @pygame_loop(pygame_env, life - 2.9) def loop3(): test_boost.draw() - if int(time()) < end: - assert most_popular_colors(test_boost.img_life, 1, [(0, 0, 0)])[0] == BoostMixin.COLOR_LONG - assert most_popular_colors(test_boost.img_life, 1, [(0, 0, 0)])[0] == BoostMixin.COLOR_SHORT # Test custom `deactivate` method From ffd0835a841f62655be93312839e011857599d04 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Tue, 30 Nov 2021 15:35:54 +0300 Subject: [PATCH 24/31] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B2=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=BA=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B5=20=D0=B1=D1=8B=D1=81=D1=82=D1=80=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=B2=D0=BE=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 4 ++-- tests/test_hitbox.py | 6 +++--- tests/test_mixins.py | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 20a68c8..4db2c73 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -30,9 +30,9 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Install dependencies - run: pip install -r requirements.txt pytest-cov + run: pip install -r requirements.txt pytest pytest-cov pytest-xdist[psutil] - name: Run tests - run: python -m pytest --cov spaceway --cov-report xml --color=yes tests/ + run: python -m pytest -n auto --cov spaceway --cov-report xml --color=yes tests/ env: SDL_VIDEODRIVER: dummy - name: Upload coverage diff --git a/tests/test_hitbox.py b/tests/test_hitbox.py index 23fb6bd..ddfe0b5 100644 --- a/tests/test_hitbox.py +++ b/tests/test_hitbox.py @@ -120,7 +120,7 @@ def test_generic(hitbox): ) assert len(hitbox) == len(PgRect(hitbox)) assert bool(hitbox) == bool(PgRect(hitbox)) - assert (hitbox == rstring()) is False + assert not (hitbox == rstring()) p = r'^<\w+\(((-?\d+|-?\d+\.\d+), ){3}(-?\d+|-?\d+\.\d+)\)>$' assert match(p, str(hitbox)) and match(p, repr(hitbox)) @@ -181,8 +181,8 @@ def test_generic(hitbox): (Ellipse(198, 190, 65, 20), NotImplementedError), (Rect(100, 123, 10, 29), NotImplementedError), (Rect(-100, 2, 40, -2), NotImplementedError), - (randint(1, 100), TypeError), - (rstring(), TypeError) + (154, TypeError), + ('qwertyasd', TypeError) ]) def test_exceptions(arg, exception): hitbox = Hitbox(100, 123, 10, 29) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index cd001ee..36cc7b9 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -149,10 +149,11 @@ def loop(): @pytest.mark.parametrize('params', [ - ( - rstring(15), - (randint(10, 120), randint(10, 120)) - ) for _ in range(5) + ('abcdefzxcv', (510, 54)), + ('asdasdafhreh', (123, 321)), + ('qwertyuiodfer', (11, 223)), + ('apodmebzx', (34, 89)), + ('poiuyaaffee', (0, -10)) ]) def test_caption_mixin(pygame_env, params): screen, base_dir, config, clock = pygame_env From cbdc18c00e1ea5fb2ec8989a7974824e844f1cb2 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Tue, 30 Nov 2021 18:46:54 +0300 Subject: [PATCH 25/31] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20GenericButtonsGroup,=20CenteredButtonsGrou?= =?UTF-8?q?p=20=D0=B8=20SceneButtonsGroup=20(collection.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/collection.py | 100 +++++++---------- tests/test_collection.py | 228 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 62 deletions(-) create mode 100644 tests/test_collection.py diff --git a/spaceway/collection.py b/spaceway/collection.py index 1d7b47a..26db4fb 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -137,7 +137,40 @@ def get(self, name): return self.active.get(name) -class CenteredButtonsGroup(pygame.sprite.Group): +class GenericButtonGroup(pygame.sprite.Group): + """Group containing methods that are inherent to all groups + working with buttons + """ + + def perform_point_collides(self, point): + """Detects collisions of buttons with the specified point. If a + collision was found, it presses on the button + + Args: + point (Tuple[int, int]): The point at which the collision is checked + + Returns: + bool: Has a collision been found + """ + # Check all buttons + for button in self: + if button.rect.collidepoint(point): + # If collision was found, press button and return `True` + button.press() + return True + + # Return `False` if no collisions were found + return False + + def draw(self) -> None: + """Updates and blits all buttons of group + """ + for button in self: + button.update() + button.blit() + + +class CenteredButtonsGroup(GenericButtonGroup): """Extension of default :group:`pygame.sprite.Group` for centering buttons of group on screen @@ -158,12 +191,12 @@ def __init__(self, mode, *buttons): """Constructor method. Adding buttons and setting of width and height of surface """ - # Initialize inherited group - pygame.sprite.Group.__init__(self, *buttons) - # Save mode of screen for the further use self.screen_width, self.screen_height = mode + # Initialize inherited group + pygame.sprite.Group.__init__(self, *buttons) + def center(self) -> None: """Centering of group """ @@ -186,26 +219,6 @@ def center(self) -> None: x += button.rect.w + self.SPACE - def perform_point_collides(self, point): - """Detects collisions of buttons with the specified point. If a - collision was found, it presses on the button - - Args: - point (Tuple[int, int]): The point at which the collision is checked - - Returns: - bool: Has a collision been found - """ - # Check all buttons - for button in self: - if button.rect.collidepoint(point): - # If collision was found, press button and return `True` - button.press() - return True - - # Return `False` if no collisions were found - return False - def add_internal(self, button) -> None: """Adding button and centering of group @@ -224,15 +237,8 @@ def remove_internal(self, button) -> None: pygame.sprite.Group.remove_internal(self, button) self.center() - def draw(self) -> None: - """Updates and blits all buttons of group - """ - for button in self: - button.update() - button.blit() - -class SceneButtonsGroup(pygame.sprite.Group): +class SceneButtonsGroup(GenericButtonGroup): """Extension of default :group:`pygame.sprite.Group` for easier control of scene buttons @@ -283,29 +289,6 @@ def remove_internal(self, button) -> None: pygame.sprite.Group.remove_internal(self, button) - def perform_point_collides(self, point): - """Detects collisions of buttons with the specified point. If a collision - was found, presses the button, leaves buttons of current scene and enters - buttons of scene to which it will be changed - - Args: - point (Tuple[int, int]): The point at which the collision is checked - - Returns: - bool: Has a collision been found - """ - # Get all buttons of current scene - for button in self.get_by_scene(): - # If collision was found - if button.rect.collidepoint(point): - # Press collided button - button.press() - - return True - - # No collisions were found - return False - def enter_buttons(self, scene='', sub_scene='') -> None: """Enters all buttons of passed scene. If no scene was passed, buttons of current scene will be entered @@ -328,13 +311,6 @@ def leave_buttons(self, scene='', sub_scene='') -> None: for button in self.get_by_scene(scene, sub_scene): button.leave() - def draw(self) -> None: - """Updates and blits all buttons of group - """ - for button in self.get_by_scene(): - button.update() - button.blit() - def get_by_scene(self, scene='', sub_scene=''): """Returns all buttons of passed scene. If no scene was selected, buttons of current scene will be returned diff --git a/tests/test_collection.py b/tests/test_collection.py new file mode 100644 index 0000000..b60274d --- /dev/null +++ b/tests/test_collection.py @@ -0,0 +1,228 @@ +from random import randint, choice + +import pytest + +from spaceway.collection import * +from spaceway.mixins import SceneButtonMixin, SettingsButtonMixin +from spaceway.hitbox import Rect + +from utils import * + + +def test_generic_buttons_group(pygame_env): + screen, base_dir, config, clock = pygame_env + + class TestSceneButton(SceneButtonMixin): + def __init__(self): + self.screen = screen + self.img = pygame_surface((randint(20, 120), randint(20, 120))) + self.rect = Rect(self.img.get_rect()) + self.rect.topleft = (randint(20, 120), randint(20, 120)) + SceneButtonMixin.__init__(self, base_dir, config, '', '', '', '') + + class TestSettingsButton(SettingsButtonMixin): + def __init__(self): + size = (randint(20, 120), randint(20, 120)) + self.imgs = {True: pygame_surface(size), + False: pygame_surface(size, 1)} + + config_index = rstring(15) + config['user'][config_index] = True + SettingsButtonMixin.__init__(self, screen, config, config_index) + + self.rect = Rect(self.img.get_rect()) + self.rect.topleft = (randint(20, 120), randint(20, 120)) + + test_group = GenericButtonGroup(*[ + TestSceneButton() if i % 2 else TestSettingsButton() + for i in range(4) + ]) + + # Test `draw` method of group + @pygame_loop(pygame_env, 1) + def loop1(): + test_group.draw() + + # Test `perform_point_collides` method of group + assert test_group.perform_point_collides(test_group.sprites()[0].rect.center) + assert not test_group.perform_point_collides((-1, -1)) + + +@pytest.mark.parametrize('buttons_sizes', [ + ((30, 45), (60, 60), (40, 60), (82, 48)), + ((120, 38), (80, 27), (30, 32), (10, 78)), + ((74, 52), (33, 48), (20, 12)) +]) +def test_centered_buttons_group(pygame_env, buttons_sizes): + screen, base_dir, config, clock = pygame_env + + class TestSceneButton(SceneButtonMixin): + def __init__(self, size): + self.screen = screen + self.img = pygame_surface(size) + self.rect = Rect(self.img.get_rect()) + self.rect.topleft = (randint(20, 120), randint(20, 120)) + SceneButtonMixin.__init__(self, base_dir, config, '', '', '', '') + + class TestSettingsButton(SettingsButtonMixin): + def __init__(self, size): + self.imgs = {True: pygame_surface(size), + False: pygame_surface(size, 1)} + + config_index = rstring(15) + config['user'][config_index] = True + SettingsButtonMixin.__init__(self, screen, config, config_index) + + self.rect = Rect(self.img.get_rect()) + self.rect.topleft = (randint(20, 120), randint(20, 120)) + + def create_buttons(): + buttons = [] + rects = [] + + for button_size in buttons_sizes: + buttons.append(choice([TestSceneButton, TestSettingsButton])(button_size)) + rects.append(buttons[-1].rect) + + return buttons, rects + + # Test centering of buttons which passed to constructor + test_buttons, buttons_rects = create_buttons() + test_group = CenteredButtonsGroup(config['mode'], *test_buttons) + + unionall_rect = buttons_rects[0].unionall(buttons_rects[1:]) + + assert tuple(map(round, unionall_rect.trunc().center)) == screen.get_rect().center + + # Test centering of buttons which added after group initialization + test_buttons, buttons_rects = create_buttons() + test_group = CenteredButtonsGroup(config['mode']) + + test_group.add(test_buttons) + unionall_rect = buttons_rects[0].unionall(buttons_rects[1:]) + + assert tuple(map(round, unionall_rect.trunc().center)) == screen.get_rect().center + + # Remove random button and check if other buttons centered again + remove_button = choice(test_buttons) + + buttons_rects.remove(remove_button.rect) + test_group.remove(remove_button) + unionall_rect = buttons_rects[0].unionall(buttons_rects[1:]) + + assert tuple(map(round, unionall_rect.trunc().center)) == screen.get_rect().center + + +@pytest.mark.parametrize('buttons_scenes', [ + ((1, 1, 2, 2), (2, 2, 1, 1)), + (('a', 'a', 'b', 'b'), ('b', 'b', 'c', 'c'), ('a', 'a', 'b', 'b')), + (('abc', 'def', 'asd', 'test'), ('abc', 'def', 'req', 'obr')) +]) +def test_scene_buttons_group(pygame_env, buttons_scenes): + screen, base_dir, config, clock = pygame_env + + class TestSceneButton(SceneButtonMixin): + def __init__(self, scenes): + self.screen = screen + self.img = pygame_surface((randint(20, 80), randint(20, 80))) + self.rect = Rect(self.img.get_rect()) + self.rect.topleft = (randint(20, 120), randint(20, 120)) + SceneButtonMixin.__init__(self, base_dir, config, *scenes) + + class UniqueTestInstance: + pass + + test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + + # Test `add` and `remove` methods of group + test_button = test_buttons[0] + test_group = SceneButtonsGroup(config) + assert len(test_group) == 0 + + test_group.add(test_button) + assert len(test_group) == 1 + + test_group.remove(test_button) + assert len(test_group) == 0 + + # Test if group with butttons which passed in constructor + # equals to group with buttons added after initialization + test_group1 = SceneButtonsGroup(config, *test_buttons) + test_group2 = SceneButtonsGroup(config) + test_group2.add(*test_buttons) + + assert len(test_group1) == len(test_group2) + + # Test `get_by_scene` method without parameters + config['scene'] = buttons_scenes[0][0] + config['sub_scene'] = buttons_scenes[0][1] + + for test_button in test_group.get_by_scene(): + assert test_button.scene == config['scene'] + assert test_button.sub_scene == config['sub_scene'] + + # Test `get_by_scene` method with parameters + test_group = SceneButtonsGroup(config, *test_buttons) + scene = buttons_scenes[1][0] + sub_scene = buttons_scenes[1][1] + + for test_button in test_group.get_by_scene(scene, sub_scene): + assert test_button.scene == scene + assert test_button.sub_scene == sub_scene + + # Test `get_by_instance` + test_group = SceneButtonsGroup(config, *test_buttons) + assert test_group.get_by_instance(TestSceneButton) == test_buttons[0] + assert test_group.get_by_instance(UniqueTestInstance) is None + + # Test `enter_buttons` method without parameters + test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_group = SceneButtonsGroup(config, *test_buttons) + config['scene'] = buttons_scenes[0][0] + config['sub_scene'] = buttons_scenes[0][1] + test_group.enter_buttons() + + for test_button in test_group: + if test_button.scene == config['scene'] and test_button.sub_scene == config['sub_scene']: + assert test_button.action == 'enter' + else: + assert test_button.action == 'stop' + + # Test `enter_buttons` method with parameters + test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_group = SceneButtonsGroup(config, *test_buttons) + scene = buttons_scenes[1][0] + sub_scene = buttons_scenes[1][1] + test_group.enter_buttons(scene, sub_scene) + + for test_button in test_group: + if test_button.scene == scene and test_button.sub_scene == sub_scene: + assert test_button.action == 'enter' + else: + assert test_button.action == 'stop' + + # Test `leave_buttons` method without parameters + test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_group = SceneButtonsGroup(config, *test_buttons) + config['scene'] = buttons_scenes[0][0] + config['sub_scene'] = buttons_scenes[0][1] + test_group.leave_buttons() + + for test_button in test_group: + if test_button.scene == config['scene'] and test_button.sub_scene == config['sub_scene']: + assert test_button.action == 'leave' + else: + assert test_button.action == 'stop' + + # Test `leave_buttons` method with parameters + test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_group = SceneButtonsGroup(config, *test_buttons) + scene = buttons_scenes[1][0] + sub_scene = buttons_scenes[1][1] + test_group.leave_buttons(scene, sub_scene) + + for test_button in test_group: + if test_button.scene == scene and test_button.sub_scene == sub_scene: + assert test_button.action == 'leave' + else: + assert test_button.action == 'stop' From 317f2986bac64c872bf8df2edc263587c9edff06 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Fri, 3 Dec 2021 19:36:59 +0300 Subject: [PATCH 26/31] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20BoostsGroup,=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D1=87=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/collection.py | 21 ++++--- tests/test_collection.py | 124 +++++++++++++++++++++++++++++++++++---- tests/test_mixins.py | 2 +- 3 files changed, 127 insertions(+), 20 deletions(-) diff --git a/spaceway/collection.py b/spaceway/collection.py index 26db4fb..239238e 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -23,14 +23,21 @@ class BoostsGroup(pygame.sprite.Group): Passive group contains other boosts in in the following format: .. code:: python - {: 0} """ + {: 0} + """ + + def __init__(self, *boosts): + """Constructor method + """ + # Define additional groups + self.active: Dict[str, 'BoostMixin'] = {} + self.passive: Dict['BoostMixin', int] = {} - # Define additional groups - active: Dict[str, 'BoostMixin'] = {} - passive: Dict['BoostMixin', int] = {} + # Define interval for next boost spawn (in score) + self.next_spawn = 3 - # Define interval for next boost spawn (in score) - next_spawn = 3 + # Initialize inherited group + pygame.sprite.Group.__init__(self, *boosts) def add_internal(self, boost) -> None: """Adds boost to passive group @@ -124,7 +131,7 @@ def activate(self, boost) -> None: boost.activate() def get(self, name): - """Searche for a boost with the passed name in active group and, + """Search for a boost with the passed name in active group and, if there is one, returns it Args: diff --git a/tests/test_collection.py b/tests/test_collection.py index b60274d..63fde6f 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -3,12 +3,99 @@ import pytest from spaceway.collection import * -from spaceway.mixins import SceneButtonMixin, SettingsButtonMixin +from spaceway.mixins import BoostMixin, SceneButtonMixin, SettingsButtonMixin from spaceway.hitbox import Rect from utils import * +@pytest.mark.parametrize('boosts_params', [ + [(7, 'a'), (3, 'b'), (5, 'c'), (7, 'a')], + [(2, 'x'), (2, 'y'), (2, 'z')], + [(9, 'q'), (3, 'f'), (5, 'r'), (4, 't')] +]) +def test_boosts_group(pygame_env, boosts_params): + screen, base_dir, config, clock = pygame_env + + class TestBoost(BoostMixin): + def __init__(self, life, name): + pygame.sprite.Sprite.__init__(self) + + self.img_idle = pygame_surface((30, 30)) + self.img_small = pygame_surface((18, 18), 1) + + BoostMixin.__init__(self, screen, base_dir, config, name, life) + + def create_boosts(): + return [TestBoost(life, name) for life, name in boosts_params] + + def uniq_len(a): + return len(set(a)) + + # Test `add` and `remove` methods of group simply + test_boosts = create_boosts() + test_boost = test_boosts[0] + test_group = BoostsGroup() + + assert len(test_group) == 0 + + test_group.add(test_boost) + assert len(test_group) == 1 + + test_group.remove(test_boost) + assert len(test_group) == 0 + + # Test `remove` method for activated boosts + test_boosts = create_boosts() + test_group = BoostsGroup(*test_boosts) + + for test_boost in test_boosts: + test_group.activate(test_boost) + + test_boost = min(test_boosts, key=lambda x: x.number_in_queue) + + assert len(test_group) == uniq_len(boosts_params) + + test_group.remove(test_boost) + assert len(test_group) == uniq_len(boosts_params) - 1 + + # Test `empty` method + test_boosts = create_boosts() + test_group = BoostsGroup(*test_boosts) + test_group.empty() + + assert len(test_group) == len(test_group.active) == len(test_group.passive) == 0 + assert test_group.next_spawn == 3 + + # Test `get` method without activated boosts + test_boosts = create_boosts() + test_boost = test_boosts[0] + test_group = BoostsGroup(*test_boosts) + + assert test_group.get(test_boost.name) is None + + # Test `get` method with activated boosts + test_boosts = create_boosts() + test_group = BoostsGroup(*test_boosts) + + for test_boost in test_boosts: + test_group.activate(test_boost) + + test_boost = test_boosts[0] + assert test_group.get(test_boost.name) + + # Test `__contains__` method + test_boosts = create_boosts() + test_group = BoostsGroup(*test_boosts) + test_boost1, test_boost2 = test_boosts[:2] + test_group.activate(test_boost1) + + assert test_boost1.name in test_group + assert test_boost1 in test_group + assert test_boost2.name not in test_group + assert test_boost2 in test_group + + def test_generic_buttons_group(pygame_env): screen, base_dir, config, clock = pygame_env @@ -17,7 +104,7 @@ def __init__(self): self.screen = screen self.img = pygame_surface((randint(20, 120), randint(20, 120))) self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(20, 120), randint(20, 120)) + self.rect.topleft = (randint(0, 550), randint(0, 250)) SceneButtonMixin.__init__(self, base_dir, config, '', '', '', '') class TestSettingsButton(SettingsButtonMixin): @@ -31,7 +118,7 @@ def __init__(self): SettingsButtonMixin.__init__(self, screen, config, config_index) self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(20, 120), randint(20, 120)) + self.rect.topleft = (randint(0, 550), randint(0, 250)) test_group = GenericButtonGroup(*[ TestSceneButton() if i % 2 else TestSettingsButton() @@ -49,9 +136,9 @@ def loop1(): @pytest.mark.parametrize('buttons_sizes', [ - ((30, 45), (60, 60), (40, 60), (82, 48)), - ((120, 38), (80, 27), (30, 32), (10, 78)), - ((74, 52), (33, 48), (20, 12)) + [(30, 45), (60, 60), (40, 60), (82, 48)], + [(120, 38), (80, 27), (30, 32), (10, 78)], + [(74, 52), (33, 48), (20, 12)] ]) def test_centered_buttons_group(pygame_env, buttons_sizes): screen, base_dir, config, clock = pygame_env @@ -61,7 +148,7 @@ def __init__(self, size): self.screen = screen self.img = pygame_surface(size) self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(20, 120), randint(20, 120)) + self.rect.topleft = (randint(0, 550), randint(0, 250)) SceneButtonMixin.__init__(self, base_dir, config, '', '', '', '') class TestSettingsButton(SettingsButtonMixin): @@ -74,7 +161,7 @@ def __init__(self, size): SettingsButtonMixin.__init__(self, screen, config, config_index) self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(20, 120), randint(20, 120)) + self.rect.topleft = (randint(0, 550), randint(0, 250)) def create_buttons(): buttons = [] @@ -86,8 +173,20 @@ def create_buttons(): return buttons, rects - # Test centering of buttons which passed to constructor + # Test `add` and `remove` methods of group test_buttons, buttons_rects = create_buttons() + test_button = test_buttons[0] + test_group = CenteredButtonsGroup(config['mode']) + + assert len(test_group) == 0 + + test_group.add(test_button) + assert len(test_group) == 1 + + test_group.remove(test_button) + assert len(test_group) == 0 + + # Test centering of buttons which passed to constructor test_group = CenteredButtonsGroup(config['mode'], *test_buttons) unionall_rect = buttons_rects[0].unionall(buttons_rects[1:]) @@ -114,9 +213,9 @@ def create_buttons(): @pytest.mark.parametrize('buttons_scenes', [ - ((1, 1, 2, 2), (2, 2, 1, 1)), - (('a', 'a', 'b', 'b'), ('b', 'b', 'c', 'c'), ('a', 'a', 'b', 'b')), - (('abc', 'def', 'asd', 'test'), ('abc', 'def', 'req', 'obr')) + [(1, 1, 2, 2), (3, 3, 1, 1), (2, 2, 1, 1)], + [('a', 'a', 'b', 'b'), ('b', 'b', 'c', 'c'), ('a', 'a', 'b', 'b')], + [('abc', 'def', 'asd', 'test'), ('abc', 'def', 'req', 'obr')] ]) def test_scene_buttons_group(pygame_env, buttons_scenes): screen, base_dir, config, clock = pygame_env @@ -137,6 +236,7 @@ class UniqueTestInstance: # Test `add` and `remove` methods of group test_button = test_buttons[0] test_group = SceneButtonsGroup(config) + assert len(test_group) == 0 test_group.add(test_button) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 36cc7b9..53229ab 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -289,7 +289,7 @@ def loop1(): def test_boost_mixin(pygame_env, life): screen, base_dir, config, clock = pygame_env - class TestBoost(BoostMixin, pygame.sprite.Sprite): + class TestBoost(BoostMixin): def __init__(self): pygame.sprite.Sprite.__init__(self) From 0d3fa0b71608769d4adac31c9fb1da35ce8e13e5 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 4 Dec 2021 15:10:52 +0300 Subject: [PATCH 27/31] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20updater.py=20(=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=84=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8,=20=D0=BF?= =?UTF-8?q?=D1=83=D1=82=D0=B5=D0=BC=20=D0=B8=D0=BC=D0=B8=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D0=B0=D1=82=D1=83=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BC=D1=8B=D1=88=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/updater.py | 2 +- tests/test_collection.py | 1 - tests/test_hitbox.py | 4 ++-- tests/test_interface.py | 34 +++++++++++++++++++++++++++++++ tests/test_scenes.py | 1 - tests/utils.py | 43 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 tests/test_interface.py diff --git a/spaceway/updater.py b/spaceway/updater.py index 1348961..b45341d 100644 --- a/spaceway/updater.py +++ b/spaceway/updater.py @@ -5,7 +5,7 @@ import pygame from packaging.version import parse from requests import get - +pygame.init() def dialog(base_dir) -> None: """ Creator of information dialog """ diff --git a/tests/test_collection.py b/tests/test_collection.py index 63fde6f..d38d234 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -5,7 +5,6 @@ from spaceway.collection import * from spaceway.mixins import BoostMixin, SceneButtonMixin, SettingsButtonMixin from spaceway.hitbox import Rect - from utils import * diff --git a/tests/test_hitbox.py b/tests/test_hitbox.py index ddfe0b5..57aaecd 100644 --- a/tests/test_hitbox.py +++ b/tests/test_hitbox.py @@ -1,10 +1,10 @@ -import pytest from random import randint from re import match -from spaceway.hitbox import Hitbox, Rect, Ellipse +import pytest from pygame import Rect as PgRect +from spaceway.hitbox import Hitbox, Rect, Ellipse from utils import rstring diff --git a/tests/test_interface.py b/tests/test_interface.py new file mode 100644 index 0000000..107d880 --- /dev/null +++ b/tests/test_interface.py @@ -0,0 +1,34 @@ +import os +from threading import Thread + +import pytest +import pygame +from pygame.event import Event + +from spaceway import main +from spaceway.updater import check_software_updates + +from utils import pygame_emulate_events + + +@pytest.mark.timeout(20) +def test_updater(): + base_dir = os.path.dirname(os.path.abspath(main.__file__)) + + # Test *View* and *Close* buttons + pygame_emulate_events( + Thread(target=check_software_updates, args=('0.0.0a', base_dir)), + [ + (Event(pygame.MOUSEBUTTONDOWN, pos=(75, 177)), 2500), # Press *View* button + (Event(pygame.MOUSEBUTTONDOWN, pos=(228, 177)), 1500), # Press *Close* button + ] + ) + + # Test if window is closed by exiting + pygame_emulate_events( + Thread(target=check_software_updates, args=('0.0.0a', base_dir)), + [(Event(pygame.QUIT), 2500)] + ) + + # Test if installed version is newer than remote + check_software_updates('999.0.0', base_dir) diff --git a/tests/test_scenes.py b/tests/test_scenes.py index 4635c08..e5dfa56 100644 --- a/tests/test_scenes.py +++ b/tests/test_scenes.py @@ -2,7 +2,6 @@ from importlib import import_module import spaceway - from utils import pygame_env diff --git a/tests/utils.py b/tests/utils.py index bcd913e..126fadd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ import os -from time import time +from time import time, sleep from random import choices from string import ascii_letters @@ -140,5 +140,46 @@ def most_popular_colors(surface, amount=1, exclude=[]): return sorted_colors[:amount] +def pygame_emulate_events(thread, events): + """Emulates pygame events (keyboard presses, mouse clicks and other) + for testing program interface + + Args: + thread (threading.Thread): Thread which targeted to the entry point of the program + events (List[Tuple[pygame.event.Event, int]]): List of tuples, where the first object + is emulated event, and the second is interval after previous event (milliseconds) + + Raises: + Exception: if thread is finished before emulating all events or + if after thread finishing there are still some events + """ + pos = (0, 0) + pygame.mouse.get_pos = lambda: pos + + thread.setDaemon(True) + thread.start() + events.reverse() + + while thread.is_alive(): + if len(events) == 0: + # Waiting for the thread to finish + sleep(1) + + if thread.is_alive(): + raise Exception('Thread is alive but there are no events!') + return + + event, wait = events.pop() + sleep(wait / 1000) + + if event.type == pygame.MOUSEBUTTONDOWN: + pos = event.pos + + pygame.event.post(event) + + if len(events): + raise Exception('Thread was finished, but some events ramain!') + + def rstring(k=5): return ''.join(choices(ascii_letters, k=k)) From 542d1fc17db79ae2750050b8d9fc12db3f9facd2 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sat, 4 Dec 2021 20:18:32 +0300 Subject: [PATCH 28/31] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BD=D0=B5=D0=B4=D0=BE=D1=87=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 2 +- spaceway/collection.py | 94 ++++++++++++-------- tests/test_collection.py | 94 ++++++++++---------- tests/{test_interface.py => test_updater.py} | 16 +++- tests/utils.py | 4 +- 5 files changed, 123 insertions(+), 87 deletions(-) rename tests/{test_interface.py => test_updater.py} (69%) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4db2c73..58be663 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Install dependencies - run: pip install -r requirements.txt pytest pytest-cov pytest-xdist[psutil] + run: pip install -r requirements.txt pytest pytest-cov pytest-xdist[psutil] pytest-timeout - name: Run tests run: python -m pytest -n auto --cov spaceway --cov-report xml --color=yes tests/ env: diff --git a/spaceway/collection.py b/spaceway/collection.py index 239238e..9e8ef52 100644 --- a/spaceway/collection.py +++ b/spaceway/collection.py @@ -144,40 +144,7 @@ def get(self, name): return self.active.get(name) -class GenericButtonGroup(pygame.sprite.Group): - """Group containing methods that are inherent to all groups - working with buttons - """ - - def perform_point_collides(self, point): - """Detects collisions of buttons with the specified point. If a - collision was found, it presses on the button - - Args: - point (Tuple[int, int]): The point at which the collision is checked - - Returns: - bool: Has a collision been found - """ - # Check all buttons - for button in self: - if button.rect.collidepoint(point): - # If collision was found, press button and return `True` - button.press() - return True - - # Return `False` if no collisions were found - return False - - def draw(self) -> None: - """Updates and blits all buttons of group - """ - for button in self: - button.update() - button.blit() - - -class CenteredButtonsGroup(GenericButtonGroup): +class CenteredButtonsGroup(pygame.sprite.Group): """Extension of default :group:`pygame.sprite.Group` for centering buttons of group on screen @@ -226,6 +193,26 @@ def center(self) -> None: x += button.rect.w + self.SPACE + def perform_point_collides(self, point): + """Detects collisions of buttons with the specified point. If a + collision was found, it presses on the button + + Args: + point (Tuple[int, int]): The point at which the collision is checked + + Returns: + bool: Has a collision been found + """ + # Check all buttons + for button in self: + if button.rect.collidepoint(point): + # If collision was found, press button and return `True` + button.press() + return True + + # Return `False` if no collisions were found + return False + def add_internal(self, button) -> None: """Adding button and centering of group @@ -244,8 +231,15 @@ def remove_internal(self, button) -> None: pygame.sprite.Group.remove_internal(self, button) self.center() + def draw(self) -> None: + """Updates and blits all buttons of group + """ + for button in self: + button.update() + button.blit() -class SceneButtonsGroup(GenericButtonGroup): + +class SceneButtonsGroup(pygame.sprite.Group): """Extension of default :group:`pygame.sprite.Group` for easier control of scene buttons @@ -296,6 +290,29 @@ def remove_internal(self, button) -> None: pygame.sprite.Group.remove_internal(self, button) + def perform_point_collides(self, point): + """Detects collisions of buttons with the specified point. If a collision + was found, presses the button, leaves buttons of current scene and enters + buttons of scene to which it will be changed + + Args: + point (Tuple[int, int]): The point at which the collision is checked + + Returns: + bool: Has a collision been found + """ + # Get all buttons of current scene + for button in self.get_by_scene(): + # If collision was found + if button.rect.collidepoint(point): + # Press collided button + button.press() + + return True + + # No collisions were found + return False + def enter_buttons(self, scene='', sub_scene='') -> None: """Enters all buttons of passed scene. If no scene was passed, buttons of current scene will be entered @@ -318,6 +335,13 @@ def leave_buttons(self, scene='', sub_scene='') -> None: for button in self.get_by_scene(scene, sub_scene): button.leave() + def draw(self) -> None: + """Updates and blits buttons current scene + """ + for button in self.get_by_scene(): + button.update() + button.blit() + def get_by_scene(self, scene='', sub_scene=''): """Returns all buttons of passed scene. If no scene was selected, buttons of current scene will be returned diff --git a/tests/test_collection.py b/tests/test_collection.py index d38d234..e28e74f 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -95,45 +95,6 @@ def uniq_len(a): assert test_boost2 in test_group -def test_generic_buttons_group(pygame_env): - screen, base_dir, config, clock = pygame_env - - class TestSceneButton(SceneButtonMixin): - def __init__(self): - self.screen = screen - self.img = pygame_surface((randint(20, 120), randint(20, 120))) - self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(0, 550), randint(0, 250)) - SceneButtonMixin.__init__(self, base_dir, config, '', '', '', '') - - class TestSettingsButton(SettingsButtonMixin): - def __init__(self): - size = (randint(20, 120), randint(20, 120)) - self.imgs = {True: pygame_surface(size), - False: pygame_surface(size, 1)} - - config_index = rstring(15) - config['user'][config_index] = True - SettingsButtonMixin.__init__(self, screen, config, config_index) - - self.rect = Rect(self.img.get_rect()) - self.rect.topleft = (randint(0, 550), randint(0, 250)) - - test_group = GenericButtonGroup(*[ - TestSceneButton() if i % 2 else TestSettingsButton() - for i in range(4) - ]) - - # Test `draw` method of group - @pygame_loop(pygame_env, 1) - def loop1(): - test_group.draw() - - # Test `perform_point_collides` method of group - assert test_group.perform_point_collides(test_group.sprites()[0].rect.center) - assert not test_group.perform_point_collides((-1, -1)) - - @pytest.mark.parametrize('buttons_sizes', [ [(30, 45), (60, 60), (40, 60), (82, 48)], [(120, 38), (80, 27), (30, 32), (10, 78)], @@ -210,6 +171,18 @@ def create_buttons(): assert tuple(map(round, unionall_rect.trunc().center)) == screen.get_rect().center + # Test `draw` method of group + test_buttons, buttons_rects = create_buttons() + test_group = CenteredButtonsGroup(config['mode'], *test_buttons) + + @pygame_loop(pygame_env, 1) + def loop1(): + test_group.draw() + + # Test `perform_point_collides` method of group + assert test_group.perform_point_collides(test_group.sprites()[0].rect.center) + assert not test_group.perform_point_collides((-1, -1)) + @pytest.mark.parametrize('buttons_scenes', [ [(1, 1, 2, 2), (3, 3, 1, 1), (2, 2, 1, 1)], @@ -220,9 +193,9 @@ def test_scene_buttons_group(pygame_env, buttons_scenes): screen, base_dir, config, clock = pygame_env class TestSceneButton(SceneButtonMixin): - def __init__(self, scenes): + def __init__(self, scenes, color): self.screen = screen - self.img = pygame_surface((randint(20, 80), randint(20, 80))) + self.img = pygame_surface((randint(20, 80), randint(20, 80)), color) self.rect = Rect(self.img.get_rect()) self.rect.topleft = (randint(20, 120), randint(20, 120)) SceneButtonMixin.__init__(self, base_dir, config, *scenes) @@ -230,7 +203,36 @@ def __init__(self, scenes): class UniqueTestInstance: pass - test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + def create_buttons(): + unique_color_scene = buttons_scenes[0][:2] + buttons = [] + + for scenes in buttons_scenes: + if scenes[:2] == unique_color_scene: + buttons.append(TestSceneButton(scenes, 0)) + else: + buttons.append(TestSceneButton(scenes, 1)) + + return buttons + + config['scene'], config['sub_scene'] = buttons_scenes[0][:2] + + # Test `draw` method + test_buttons = create_buttons() + test_group = SceneButtonsGroup(config, *test_buttons) + + @pygame_loop(pygame_env, 1) + def loop1(): + test_group.draw() + + assert ( + most_popular_colors(screen, exclude=[(0, 0, 0)])[0] == (0, 57, 255) or + most_popular_colors(screen, exclude=[(0, 0, 0)])[0] == (255, 46, 222) + ) + + # Test `perform_point_collides` method of group + assert test_group.perform_point_collides(test_group.sprites()[0].rect.center) + assert not test_group.perform_point_collides((-1, -1)) # Test `add` and `remove` methods of group test_button = test_buttons[0] @@ -275,7 +277,7 @@ class UniqueTestInstance: assert test_group.get_by_instance(UniqueTestInstance) is None # Test `enter_buttons` method without parameters - test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_buttons = create_buttons() test_group = SceneButtonsGroup(config, *test_buttons) config['scene'] = buttons_scenes[0][0] config['sub_scene'] = buttons_scenes[0][1] @@ -288,7 +290,7 @@ class UniqueTestInstance: assert test_button.action == 'stop' # Test `enter_buttons` method with parameters - test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_buttons = create_buttons() test_group = SceneButtonsGroup(config, *test_buttons) scene = buttons_scenes[1][0] sub_scene = buttons_scenes[1][1] @@ -301,7 +303,7 @@ class UniqueTestInstance: assert test_button.action == 'stop' # Test `leave_buttons` method without parameters - test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_buttons = create_buttons() test_group = SceneButtonsGroup(config, *test_buttons) config['scene'] = buttons_scenes[0][0] config['sub_scene'] = buttons_scenes[0][1] @@ -314,7 +316,7 @@ class UniqueTestInstance: assert test_button.action == 'stop' # Test `leave_buttons` method with parameters - test_buttons = [TestSceneButton(scenes) for scenes in buttons_scenes] + test_buttons = create_buttons() test_group = SceneButtonsGroup(config, *test_buttons) scene = buttons_scenes[1][0] sub_scene = buttons_scenes[1][1] diff --git a/tests/test_interface.py b/tests/test_updater.py similarity index 69% rename from tests/test_interface.py rename to tests/test_updater.py index 107d880..fd5872b 100644 --- a/tests/test_interface.py +++ b/tests/test_updater.py @@ -1,4 +1,5 @@ import os +import socket from threading import Thread import pytest @@ -12,23 +13,32 @@ @pytest.mark.timeout(20) -def test_updater(): +def test_updater(monkeypatch): base_dir = os.path.dirname(os.path.abspath(main.__file__)) # Test *View* and *Close* buttons pygame_emulate_events( + monkeypatch, Thread(target=check_software_updates, args=('0.0.0a', base_dir)), [ (Event(pygame.MOUSEBUTTONDOWN, pos=(75, 177)), 2500), # Press *View* button - (Event(pygame.MOUSEBUTTONDOWN, pos=(228, 177)), 1500), # Press *Close* button - ] + (Event(pygame.MOUSEBUTTONDOWN, pos=(228, 177)), 3000), # Press *Close* button + ], ) # Test if window is closed by exiting pygame_emulate_events( + monkeypatch, Thread(target=check_software_updates, args=('0.0.0a', base_dir)), [(Event(pygame.QUIT), 2500)] ) # Test if installed version is newer than remote check_software_updates('999.0.0', base_dir) + + # Test if there is no internet connection + def guard(*args, **kwargs): + raise Exception('Network error') + + monkeypatch.setattr(socket, 'socket', guard) + check_software_updates('0.0.0a', base_dir) diff --git a/tests/utils.py b/tests/utils.py index 126fadd..57d9c61 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -140,7 +140,7 @@ def most_popular_colors(surface, amount=1, exclude=[]): return sorted_colors[:amount] -def pygame_emulate_events(thread, events): +def pygame_emulate_events(monkeypatch, thread, events): """Emulates pygame events (keyboard presses, mouse clicks and other) for testing program interface @@ -154,7 +154,7 @@ def pygame_emulate_events(thread, events): if after thread finishing there are still some events """ pos = (0, 0) - pygame.mouse.get_pos = lambda: pos + monkeypatch.setattr(pygame.mouse, 'get_pos', lambda: pos) thread.setDaemon(True) thread.start() From ad6b7b89c5f8f49eb90ed90a44e0b282d05ae3c7 Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Sun, 12 Dec 2021 15:38:20 +0300 Subject: [PATCH 29/31] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D1=81=D1=86=D0=B5=D0=BD=D0=B5=20?= =?UTF-8?q?headpiece=20=D0=B4=D0=BB=D1=8F=20FPS=20=D0=BD=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spaceway/scenes/headpiece/objects.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spaceway/scenes/headpiece/objects.py b/spaceway/scenes/headpiece/objects.py index d80ad56..f06b974 100644 --- a/spaceway/scenes/headpiece/objects.py +++ b/spaceway/scenes/headpiece/objects.py @@ -15,16 +15,19 @@ def __init__(self, screen, base_dir, config): self.img_heart = pygame.image.load(f'{base_dir}/assets/images/heart/heart.bmp') self.rect_heart = self.img_heart.get_rect() self.is_heart = False + self.tick = 0 self.base_dir = base_dir CaptionMixin.__init__(self, base_dir, config, 'YariKartoshe4ka') def update(self): - if self.config['ns'].tick % (self.config['FPS'] * 4) == 0: + self.tick += self.config['ns'].dt / 30 + + if self.tick > 4: self.config['scene'] = self.config['sub_scene'] = 'lobby' - elif self.config['ns'].tick % (self.config['FPS'] * 2) == 0: + elif self.tick > 2 and not self.is_heart: self.caption = 'With love' self.is_heart = True @@ -62,14 +65,13 @@ def __init__(self, screen, base_dir, config): )[self.config['user']['color']] self.line = Rect(0, self.config['mode'][1] - 5, 0, 5) - self.inc = self.config['mode'][0] / (self.config['FPS'] * 4) self.font = pygame.font.Font(f'{base_dir}/assets/fonts/pixeboy.ttf', 22) def update(self): - self.line.width += self.inc + self.line.width += self.config['ns'].dt * self.config['mode'][0] / 120 - self.img = self.font.render(f"{round(self.line.width * 100 / self.config['mode'][0])}%", True, self.color) + self.img = self.font.render(f"{min(100, round(self.line.width / self.config['mode'][0] * 100))}%", True, self.color) self.rect = self.img.get_rect() self.rect.centerx = max( From 1160d8758208e8e278426fe33adc97ac00040e0b Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Tue, 14 Dec 2021 16:10:54 +0300 Subject: [PATCH 30/31] =?UTF-8?q?=D0=A3=D1=81=D0=BA=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=20=D0=BE=D1=82?= =?UTF-8?q?=20FPS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++-- spaceway/scenes/game/__init__.py | 1 + spaceway/scenes/game/functions.py | 5 ++++- spaceway/scenes/settings/functions.py | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 332b1f7..6859013 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ venv __pycache__ *.pyc -.p4a other -tmp build dist *.egg-info .coverage +bin +.buildozer diff --git a/spaceway/scenes/game/__init__.py b/spaceway/scenes/game/__init__.py index cf9882e..e8e4c70 100644 --- a/spaceway/scenes/game/__init__.py +++ b/spaceway/scenes/game/__init__.py @@ -3,6 +3,7 @@ def init(screen, base_dir, config): config['ns'].speed = 2 + config['ns'].current_time = 0 config['ns'].score = 0 bg = Background(screen, base_dir, config) diff --git a/spaceway/scenes/game/functions.py b/spaceway/scenes/game/functions.py index 65c8053..53eb460 100644 --- a/spaceway/scenes/game/functions.py +++ b/spaceway/scenes/game/functions.py @@ -119,7 +119,9 @@ def update(screen, config, base_dir, bg, plate, astrs, boosts, score, end, pause scene_buttons.draw() - if config['ns'].tick % (config['FPS'] * 7) == 0: + config['ns'].current_time += config['ns'].dt / 30 + if config['ns'].current_time > 7: + config['ns'].current_time = 0 if 'time' in boosts: boosts.get('time').speed += 1 else: @@ -213,6 +215,7 @@ def defeat(plate, astrs, boosts, end, config, base_dir): boosts.empty() config['ns'].speed = 2 + config['ns'].current_time = 0 config['ns'].score = 0 config['scene'] = 'game' config['sub_scene'] = 'end' diff --git a/spaceway/scenes/settings/functions.py b/spaceway/scenes/settings/functions.py index d1aef13..888c30a 100644 --- a/spaceway/scenes/settings/functions.py +++ b/spaceway/scenes/settings/functions.py @@ -23,10 +23,11 @@ def check_events(config, scene_buttons, settings_buttons, nick): config.save() if nick.rect.collidepoint((x, y)): - print('click nick!') nick.is_enable = True + pygame.key.start_text_input() else: nick.is_enable = False + pygame.key.stop_text_input() elif event.type == pygame.KEYDOWN and nick.is_enable: if event.key == pygame.K_BACKSPACE: From d8a85f8165acd3f0aae0688685e5858645b8720d Mon Sep 17 00:00:00 2001 From: YariKartoshe4ka Date: Wed, 15 Dec 2021 16:55:22 +0300 Subject: [PATCH 31/31] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D1=80=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 35 +++++++++++++++++++++++++++++++++++ README.md | 18 ++++++++++++------ docs/CODESTYLE.md | 2 +- docs/UPDATE.md | 8 +++++++- setupfiles/Space Way.desktop | 2 +- spaceway/config/config.json | 2 +- 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd878ed..269ef5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,41 @@ jobs: asset_name: "Space-Way-${{ needs.create-release.outputs.tag_name }}.exe" asset_content_type: application/exe + build-android: + name: Build for Android + runs-on: ubuntu-latest + needs: create-release + steps: + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: android + - name: Configure payloads + run: | + VERSION=$(jq < spaceway/config/config.json .version -r) + sed -i "s/#VERSION#/$VERSION/" buildozer.spec + sed -i "s/#VERSION#/'$VERSION'/" setupfiles/android/p4a_recipes/spaceway/__init__.py + - name: Build APK + uses: ArtemSBulgakov/buildozer-action@v1 + id: buildozer + with: + command: | + python3 setup.py sdist + PATH_TO_PACKAGES=$(pwd)/dist buildozer android debug + - name: Upload APK + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_API_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ${{ steps.buildozer.outputs.filename }} + asset_name: "Space-Way-${{ needs.create-release.outputs.tag_name }}.apk" + asset_content_type: application/vnd.android.package-archive + publish-pypi: name: Publish on PyPI runs-on: ubuntu-latest diff --git a/README.md b/README.md index 769dcf7..a11109d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Arcade game about space, in which you must overcome the space path by flying around obstacles -![](https://img.shields.io/pypi/v/spaceway) ![](https://img.shields.io/github/release-date/YariKartoshe4ka/Space-Way) ![](https://img.shields.io/pypi/dm/spaceway) +![](https://img.shields.io/pypi/v/spaceway) ![](https://img.shields.io/github/release-date/YariKartoshe4ka/Space-Way) ![](https://img.shields.io/pypi/dm/spaceway)
+![](https://img.shields.io/codecov/c/github/YariKartoshe4ka/Space-Way) ![](https://img.shields.io/github/issues-raw/YariKartoshe4ka/Space-Way/help%20wanted) @@ -13,11 +14,16 @@ Arcade game about space, in which you must overcome the space path by flying aro ### Installation -###### Compiled (Only Windows) +###### Compiled (Windows 10+) -1. Download Space Way installer from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) -2. Launch the installer and install Space Way -3. Launch the program with a shortcut +1. Download Space Way binary from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) (ends on **.exe**) +2. Launch program and play! + +###### Compiled (Android 5.0+) + +1. Download Space Way package file from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) (ends on **.apk**) +2. Launch it and install Space Way +3. Run game with shortcut and play! ###### Via pip (All platforms) @@ -36,7 +42,6 @@ I am not a professional game developer (this is my first game), and I do not kno 1. Music and sprites update 2. Add more obstacles for different levels of difficulty -3. Add support of Android (very far future, >100 stars on this repo) [and many other things...](https://github.com/YariKartoshe4ka/Space-Way/blob/master/docs/TODO.md) @@ -45,6 +50,7 @@ I am not a professional game developer (this is my first game), and I do not kno If you want to contribute to this repo, check out [TODO.md](https://github.com/YariKartoshe4ka/Space-Way/blob/master/docs/TODO.md) and check out what you can do
I am currently looking for artists to evaluate and rework sprites
+You can also look at the issue marked as *"help wanted"* and help to solve them
I welcome information about bugs, ideas and suggestions, always open for issue and pull requests
diff --git a/docs/CODESTYLE.md b/docs/CODESTYLE.md index 324058b..07c605e 100644 --- a/docs/CODESTYLE.md +++ b/docs/CODESTYLE.md @@ -33,7 +33,7 @@ General files: - *collection.py* - file with the implementation of additional data structures, mainly the *pygame.sprite.Group* extensions - *config.py* - file with some objects for easier configuration management - *debug.py* - file with some objects for easier debugging game -- *hitbox.py* - аile with implementation of hitboxes for some objects calculations +- *hitbox.py* - file with implementation of hitboxes for some objects calculations - *main.py* - main file, import all modules, contains the entrypoint of game and connects all the scenes together - *mixins.py* - file with mixins which are needed for simple creation of the same type of objects (DRY principle) - *updater.py* - file responsible for updating Space Way diff --git a/docs/UPDATE.md b/docs/UPDATE.md index f5c1510..eac54b4 100644 --- a/docs/UPDATE.md +++ b/docs/UPDATE.md @@ -5,9 +5,15 @@ ###### Windows -1. Download new Space Way binary from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) (installer ends on **.exe**) +1. Download new Space Way binary from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) (ends on **.exe**) 2. Launch program and play! +###### Android + +1. Download new Space Way package from [latest releases](https://github.com/YariKartoshe4ka/Space-Way/releases/latest) (ends on **.apk**) +2. Launch, it will offer you to update Space Way, you should agree +3. Run game with shortcut and play! + ###### Other 1. Update Sapce Way via PIP diff --git a/setupfiles/Space Way.desktop b/setupfiles/Space Way.desktop index 8fcd7d8..1de3493 100755 --- a/setupfiles/Space Way.desktop +++ b/setupfiles/Space Way.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=2.0.0 +Version=2.2.0 Type=Application Name=Space Way Comment=Arcade game about space, in which you must overcome the space path by flying around obstacles diff --git a/spaceway/config/config.json b/spaceway/config/config.json index 8978d5b..8030680 100644 --- a/spaceway/config/config.json +++ b/spaceway/config/config.json @@ -4,6 +4,6 @@ "FPS": 60, "scene": "headpiece", "sub_scene": "headpiece", - "version": "2.1.0", + "version": "2.2.0", "debug": false } \ No newline at end of file