48 Commits

Author SHA1 Message Date
Giovani Rodriguez
a91f937cb9 feat: improve msi build script 2021-07-16 22:26:19 +00:00
Giovani Rodriguez
a0fdddaed8 chore: change pygame version in requirements 2021-07-16 17:28:24 -04:00
Giovani Rodriguez
e895e4aafe chore: add cx_freeze requirement 2021-07-16 17:27:46 -04:00
Giovani Rodriguez
e3ce0bdd90 fix: address missing files and modules in build 2021-07-16 17:27:02 -04:00
Giovani Rodriguez
43a8588c68 feat: add game over modal 2021-07-16 17:08:55 -04:00
Giovani Rodriguez
173882d323 fix: address issue with unsync color and squares 2021-07-16 15:07:08 -04:00
Giovani Rodriguez
609b625e06 chore: remove print from online module 2021-07-15 23:29:08 -04:00
Giovani Rodriguez
736570d13b feat: add ability to send stats to server 2021-07-15 23:28:36 -04:00
Giovani Rodriguez
aacb429413 Merge branch 'main' of github.com:gio101046/tetri5 2021-07-15 20:40:30 -04:00
Giovani Rodriguez
69dbf05371 fix: address issue with colors on opponent side 2021-07-15 20:40:26 -04:00
Giovani Rodriguez
ce3beb3c93 chore: add trello to readme 2021-07-15 19:34:31 -04:00
Giovani Rodriguez
cfc6054f25 wip: add stack and piece communication 2021-07-15 18:58:27 -04:00
Giovani Rodriguez
dba6352510 feat: add wall kick off the top of well 2021-07-15 10:37:09 -04:00
Giovani Rodriguez
de115732d0 fix: address issue with levels not incrementing 2021-07-15 10:36:34 -04:00
Giovani Rodriguez
9bdedcc4d9 feat: complete connection scene 2021-07-14 19:38:55 -04:00
Giovani Rodriguez
219f1cb8d0 chore: update readme with logo 2021-07-14 19:34:08 -04:00
Giovani Rodriguez
ed1f3f984d chore: update readme gif 2021-07-14 18:55:59 -04:00
Giovani Rodriguez
d34df3479b wip: add connection to server scene 2021-07-14 15:21:53 -04:00
Giovani Rodriguez
d1eb3b34d9 feat: improve piece function and add wall kick 2021-07-14 13:54:39 -04:00
Giovani Rodriguez
3e18643735 feat: swap out the current logo with a unique one 2021-07-14 10:08:06 -04:00
Giovani Rodriguez
a5eeb39993 chore: code clean up 2021-07-13 15:15:18 -04:00
Giovani Rodriguez
c134a14d3c feat: add player two functionality 2021-07-13 15:11:35 -04:00
Giovani Rodriguez
e62da4394e feat: add multiplayer piece generation 2021-07-13 11:58:52 -04:00
Giovani Rodriguez
b23baed662 wip: add items to multiplayer scene 2021-07-13 10:45:23 -04:00
Giovani Rodriguez
0b2dc54476 feat: add piece color to the stack 2021-07-12 19:33:19 -04:00
Giovani Rodriguez
96b0f24132 chore: update readme 2021-07-10 14:20:00 -04:00
Giovani Rodriguez
3d95becbcd wip: add multiplayer screen 2021-07-09 09:13:14 -04:00
Giovani Rodriguez
655777786f refactor: move hardcoded positions into config 2021-07-08 14:35:17 -04:00
Giovani Rodriguez
a1ee32d847 feat: add ability to hard drop 2021-07-08 14:02:25 -04:00
Giovani Rodriguez
de9a9dab95 feat: improve game music 2021-07-08 11:23:47 -04:00
Giovani Rodriguez
87efe0915a feat: add single player scene 2021-07-08 01:20:33 -04:00
Giovani Rodriguez
7c4934028e fix: address music not playing 2021-07-07 21:53:55 -04:00
Giovani Rodriguez
f822a211de feat!: add title screen scene 2021-07-07 21:47:55 -04:00
Giovani Rodriguez
1de1ae996c chore: add required python packages in file 2021-07-07 15:09:54 -04:00
Giovani Rodriguez
91ebee74f2 wip!: add scenes to the game 2021-07-07 14:49:04 -04:00
Giovani Rodriguez
27c49896bf refactor: remove unneeded pass 2021-07-06 15:16:05 -04:00
Giovani Rodriguez
8e595b0bdd chore: add lgtm badges to readme 2021-07-06 15:14:57 -04:00
Giovani Rodriguez
2fb5297526 fix: address issue with main music on win10 2021-07-06 15:14:38 -04:00
Giovani Rodriguez
8a72d381c8 feat: add exit game server message 2021-07-06 13:57:43 -04:00
Giovani Rodriguez
3af770fc7b feat: improve performance on receive message 2021-07-06 13:14:47 -04:00
Giovani Rodriguez
3a85322d2f chore: change license 2021-07-06 12:29:42 -04:00
Giovani Rodriguez
8687ea3a26 feat: add client id to connection 2021-07-06 01:59:14 -04:00
Giovani Rodriguez
03d4a6afef feat: expand network connection implementation 2021-07-06 00:19:59 -04:00
Giovani Rodriguez
5425d4a7fc feat: add ability to close connection 2021-07-05 12:44:29 -04:00
Giovani Rodriguez
cd0ee0f2d6 feat: improve websocket connection performance 2021-07-05 12:04:00 -04:00
Giovani Rodriguez
4277800f09 feat: build server connection backbone 2021-07-02 20:51:10 -04:00
Giovani Rodriguez
809f591300 chore: update readme 2021-07-02 20:50:57 -04:00
Giovani Rodriguez
ba32dd1681 feat: test connection to backend 2021-06-29 06:18:33 +00:00
30 changed files with 1735 additions and 674 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Giovani Rodriguez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,9 +0,0 @@
Copyright (c) 2021 Giovani Rodriguez
Permission is hereby granted to any persons and/or organizations using this software to copy, modify, merge, publish, and distribute it. Said persons and/or organizations are not allowed to use the software or any derivatives of the work for commercial use or any other means to generate income, nor are they allowed to claim this software as their own.
The persons and/or organizations are also disallowed from sub-licensing and/or trademarking this software without explicit permission from the author.
Any persons and/or organizations using this software must disclose their source code and have it publicly available, include this license, provide sufficient credit to the original authors of the project (IE: Giovani Rodriguez), as well as provide a link to the original project.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,22 +1,36 @@
# Python Tetris Clone <p align="center">
<img width="320" height="288" src="https://github.com/gio101046/tetri5/blob/main/resource/image/title_screen.png?raw=true">
</p>
The python-tetris-clone project was created for the purpose of becoming familiar with Python 3. The goal is to replicate the most current version of Tetris (https://tetris.com/play-tetris) down to every mechanic. This includes the way pieces rotate and even newer features such as the ghost piece. [![Total alerts](https://img.shields.io/lgtm/alerts/g/gio101046/tetri5.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/gio101046/tetri5/alerts/)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/gio101046/tetri5.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/gio101046/tetri5/context:python)
[![Trello](https://img.shields.io/badge/-Trello-blue?style=for-the-badge&logo=trello)](https://trello.com/b/3BxntLgZ/tetri5)
The game was created using the pygame python library and builds on Github are packaged into one executable using cx_Freeze. The tetri5 project was created for the purpose of becoming familiar with Python 3. The game was created using the pygame python library and builds on Github are packaged into one executable using cx_Freeze.
The goal is to implement the features of the current version of Tetris (https://tetris.com/play-tetris) and add some extra ones with a unique twist.
### Feature List
- [x] Rotate Right
- [ ] Rotate Left
- [x] Soft Drop
- [x] Hard Drop
- [x] Lock Delay
- [x] Wall Kick
- [x] Next Piece
## Demo ## Demo
![gif](https://i.imgur.com/pWAs0qX.gif) ![gif](https://i.imgur.com/7QbL08C.gif)
## Requirements ## Requirements
Make sure you have installed Python 3. If you have not, download and install Python 3 at [python.org](https://www.python.org/downloads/). Make sure you have installed Python 3. If you have not, download and install Python 3 at [python.org](https://www.python.org/downloads/).
Then use the package manager [pip](https://pip.pypa.io/en/stable/) to install the required packages. Then use the package manager [pip](https://pip.pypa.io/en/stable/) to install the required dependencies.
```bash ```bash
pip install pygame pip install -r requirements.txt
pip install pyyaml
``` ```
## Run the Game ## Run the Game
@@ -35,3 +49,6 @@ python3 main.py
## Contributing ## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@@ -5,32 +5,68 @@ base = None
if sys.platform == 'win32': if sys.platform == 'win32':
base = "Win32GUI" base = "Win32GUI"
# TODO add icon to cx freeze set up shortcut_table = [
("DesktopShortcut", # Shortcut
"DesktopFolder", # Directory_
"Tetri5", # Name
"TARGETDIR", # Component_
"[TARGETDIR]main.exe", # Target
None, # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
'TARGETDIR' # WkDir
)
]
msi_data = {"Shortcut": shortcut_table}
bdist_msi_options = {'data': msi_data}
setup( setup(
name="Tetris", name="Tetri5",
version="0.7.0", version="1.0.0",
description="Tetris Python Clone", description="A full featured tetris game using python's pygame library. https://github.com/gio101046/tetri5",
options={ options={
"bdist_msi": bdist_msi_options,
"build_exe": { "build_exe": {
"packages": [ "packages": [
"pygame", "pygame",
"typing", "typing",
"types",
"copy", "copy",
"yaml", "yaml",
"random", "random",
"sys", "sys",
"tetris.entity", "asyncio",
"tetris.game", "websockets",
"tetris.util" "json",
"queue",
"uuid",
"threading",
"tetri5.entity",
"tetri5.game",
"tetri5.modal",
"tetri5.online",
"tetri5.scene",
"tetri5.util"
], ],
"include_files": [ "include_files": [
("config.yaml", "config.yaml"), ("config.yaml", "config.yaml"),
("resource/image/tetris_icon.png", "resource/image/tetris_icon.png"),
("resource/image/press-start-2p-font.bmp", "resource/image/press-start-2p-font.bmp"), ("resource/image/press-start-2p-font.bmp", "resource/image/press-start-2p-font.bmp"),
("resource/sound/main_music.ogg", "resource/sound/main_music.ogg"), ("resource/image/tetris_icon.png", "resource/image/tetris_icon.png"),
("resource/sound/piece_set_3.wav", "resource/sound/piece_set_3.wav"), ("resource/image/title_screen.png", "resource/image/title_screen.png"),
("resource/sound/row_completion.wav", "resource/sound/row_completion.wav"), ("resource/sound/four_lines_complete.ogg", "resource/sound/four_lines_complete.ogg"),
("resource/sound/game_over.ogg", "resource/sound/game_over.ogg"),
("resource/sound/level_up.ogg", "resource/sound/level_up.ogg"),
("resource/sound/line_complete.ogg", "resource/sound/line_complete.ogg"),
("resource/sound/option_change.wav", "resource/sound/option_change.wav"),
("resource/sound/piece_rotate.ogg", "resource/sound/piece_rotate.ogg"),
("resource/sound/piece_set.ogg", "resource/sound/piece_set.ogg"),
("resource/sound/theme_music_multi.ogg", "resource/sound/theme_music_multi.ogg"),
("resource/sound/theme_music_single.ogg", "resource/sound/theme_music_single.ogg"),
("resource/sound/you_win.ogg", "resource/sound/you_win.ogg")
] ]
} }
}, },

View File

@@ -1,26 +1,74 @@
window: window:
width: 800 width: 800
height: 600 height: 600
title: "Tetri5"
title: "Tetris"
sound: sound:
main-music: "resource/sound/main_music.ogg" theme-music-single: "resource/sound/theme_music_single.ogg"
row-completion: "resource/sound/row_completion.wav" theme-music-multi: "resource/sound/theme_music_multi.ogg"
piece-set: "resource/sound/piece_set_3.wav" option-change: "resource/sound/option_change.wav"
piece-rotate: "resource/sound/piece_rotate.ogg"
piece-set: "resource/sound/piece_set.ogg"
line-complete: "resource/sound/line_complete.ogg"
four-lines-complete: "resource/sound/four_lines_complete.ogg"
level-up: "resource/sound/level_up.ogg"
game-over: "resource/sound/game_over.ogg"
you-win: "resource/sound/you_win.ogg"
image: image:
title-screen: "resource/image/title_screen.png"
window-icon: "resource/image/tetris_icon.png" window-icon: "resource/image/tetris_icon.png"
font: "resource/image/press-start-2p-font.bmp" font: "resource/image/press-start-2p-font.bmp"
position:
# title scene
title-logo: [240, 60]
option-one: [320, 390]
option-two: [320, 440]
cursor-option-one: [300, 399]
cursor-option-two: [300, 449]
# single player game scene
top-label: [80, 120]
top-value: [80, 140]
score-label: [80, 180]
score-value: [80, 200]
lines-label: [300, 40]
next-label: [600, 120]
level-label: [600, 220]
well: [280, 80]
next-piece: [620, 160]
spawn-piece: [-260, -60]
# connection scene
game-id-label: [200, 300]
connecting-label: [280, 300]
waiting-for-opponent-label: [200, 300]
# multi player game scene
well-player-1: [80, 80]
well-player-2: [480, 80]
score-label-player-1: [140, 20]
score-label-player-2: [540, 20]
score-value-player-1: [140, 40]
score-value-player-2: [540, 40]
lines-label-player-1: [100, 540]
lines-label-player-2: [500, 540]
next-label-player-1: [360, 120]
next-piece-player-1: [360, 160]
spawn-piece-player-1: [-200, -60]
spawn-piece-player-2: [200, -360]
engine: engine:
fps: 60 fps: 60
tile-size: 20 tile-size: 20
piece-gravity-time: 600 cursor-blink-interval: 150
piece-set-time: 600 period-blink-interval: 300
piece-gravity-increase: 56
lines-per-level: 5 lines-per-level: 5
points-per-lines-completed: ping-interval: 1500
# piece
piece-drop-delay: 600
piece-lock-delay: 1000
piece-drop-delay-decrease: 56
# points
points-table:
- 0 # 0 line - 0 # 0 line
- 40 # 1 line - 40 # 1 line
- 100 # 2 lines - 100 # 2 lines
@@ -28,13 +76,26 @@ engine:
- 1200 # 4 lines - 1200 # 4 lines
color: color:
# window
window-bg: "#000000" window-bg: "#000000"
piece-1: "#1F37EC" # title screen
piece-2: "#3DBBFC" cursor: "#FFFFFF"
piece-3: "#FFFFFF" # in game
piece-inner-border-1: "#1F37EC" well-1: "#000000"
well-border-1: "#9BFCF0"
well-2: "#9BFCF0"
well-border-2: "#000000"
piece-1-player-1: "#1F37EC"
piece-2-player-1: "#5BDB57"
piece-3-player-1: "#F5F5F5"
piece-inner-border-1-player-1: "#1F37EC"
piece-1-player-2: "#F83801"
piece-2-player-2: "#7F7F7F"
piece-3-player-2: "#F5F5F5"
piece-inner-border-1-player-2: "#F83801"
piece-outer-border-1: "#000000" piece-outer-border-1: "#000000"
well-1: "#9BFCF0" piece-ghost: "#9BFCF0"
well-border-1: "#000000"
stack-1: "#747474" online:
stack-border-1: "#000000" server-url: "ws://webapi.tetri5.com"
#server-url: "ws://localhost:5001"

14
main.py
View File

@@ -4,18 +4,16 @@
https://tetris.com/play-tetris https://tetris.com/play-tetris
''' '''
from tetris.game import Game from tetri5.game import Game
from tetris.util import ConfigurationManager from tetri5.util import ConfigurationManager
def main() -> None: def main() -> None:
ConfigurationManager.load() ConfigurationManager.init()
Game.init()
game = Game()
game.initialize()
while True: while True:
game.update() Game.update()
game.draw() Game.draw()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
cx-Freeze==6.7
importlib-metadata==4.6.1
pygame==2.0.1
PyYAML==5.4.1
websockets==9.1
zipp==3.5.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Binary file not shown.

BIN
resource/sound/level_up.ogg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resource/sound/you_win.ogg Normal file

Binary file not shown.

0
tetri5/__init__.py Normal file
View File

492
tetri5/entity.py Normal file
View File

@@ -0,0 +1,492 @@
import copy
import random
import pygame
from typing import List, Tuple
from types import FunctionType
from tetri5.util import ConfigurationManager
from tetri5.util import Controller
from tetri5.util import SoundManager
"""
TODO description
"""
class Entity:
def __init__(self, points: Tuple, base_color: str, outer_color: str = None):
self._tile_size = ConfigurationManager.get("engine", "tile-size")
self._points = points
self._base_color = base_color
self._outer_color = outer_color
self._elapsed_time = 0 # TODO does nothing right now
def update(self, elapsed_time: int) -> None:
self._elapsed_time += elapsed_time
def draw(self, surface: pygame.Surface) -> None:
for square in self._points:
if self._base_color is not None:
square_color = pygame.Color(self._base_color)
pygame.draw.polygon(surface, square_color, square, 0)
if self._outer_color is not None:
pygame.draw.polygon(surface, pygame.Color(self._outer_color), square, max(self._tile_size // 6, 1))
def collide(self, entity: "Entity") -> bool: # TODO figure out how to do type hint for entity param of type Entity
for square_one in self._points:
for square_two in entity._points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
def _collide(self, points: List) -> bool:
for square_one in self._points:
for square_two in points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
"""
TODO description
"""
class Well(Entity):
WIDTH = 10 # standard tetris well width, should not be changed
HEIGHT = 20 # standard tetris well height, should not be changed
def __init__(self, position: Tuple, base_color: str, outer_color: str):
super().__init__(Well._get_points(position), base_color, outer_color)
@classmethod
def _get_points(cls, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
shape = []
for i in range(cls.WIDTH + 2):
for j in range(cls.HEIGHT + 2):
if i in [0, cls.WIDTH + 1]:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
elif j in [0, cls.HEIGHT + 1]:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
points = []
for square in shape:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
'''
For information on the Tetris piece Tetromino go here:
https://tetris.fandom.com/wiki/Tetromino
'''
class Piece(Entity):
def __init__(self, shape: Tuple, position: Tuple, base_color: str, inner_color: str, outer_color: str):
super().__init__(Piece._get_points(shape, position), base_color, outer_color)
self._inner_color = inner_color
self._ghost_piece_base_color = ConfigurationManager.get("color", "piece-ghost")
self._center = self._get_center(shape, position)
self._previous_points = None
self._previous_center = None
# Drop delay
self._drop_delay = ConfigurationManager.get("engine", "piece-drop-delay") # in ms
self._drop_delay_acc = 0 # acumulated drop delay, in ms
self._applying_drop_delay = True
# Lock delay
self._lock_delay = ConfigurationManager.get("engine", "piece-lock-delay") # in ms
self._lock_delay_acc = 0 # acumulated lock delay, in ms
self._applying_lock_delay = False
def update(self, elapsed_time: int, well: Well, stack: "Stack", level: int, clear_current_piece: FunctionType) -> None:
super().update(elapsed_time)
# handle rotation, left and right movement
if Controller.key_down(pygame.K_UP):
if self.rotate_with_wall_kick(well, stack):
SoundManager.play_piece_rotate_sfx()
if Controller.key_down(pygame.K_LEFT):
self.move((-self._tile_size, 0))
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
if Controller.key_down(pygame.K_RIGHT):
self.move((self._tile_size, 0))
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
# handle soft drop movement and gravity based on level
start_drop_delay = ConfigurationManager.get("engine", "piece-drop-delay")
start_lock_delay = ConfigurationManager.get("engine", "piece-lock-delay")
drop_delay_decrease = ConfigurationManager.get("engine", "piece-drop-delay-decrease")
if Controller.key_pressed(pygame.K_DOWN):
self._drop_delay = max(10, (start_drop_delay - (level * drop_delay_decrease)) // 10)
self._set_time = max(10, start_lock_delay // 10)
if not Controller.key_pressed(pygame.K_DOWN):
self._drop_delay = start_drop_delay - (level * drop_delay_decrease)
self._set_time = start_lock_delay
# handle hard drop
if Controller.key_down(pygame.K_SPACE):
self._points = self._get_ghost_piece_points(well, stack)
self._add_to_stack(stack, clear_current_piece)
if self._applying_drop_delay:
self._applying_drop_delay = self._apply_drop(elapsed_time, well, stack)
self._applying_lock_delay = not self._applying_drop_delay
"""
For more information on the piece set logic go here:
https://strategywiki.org/wiki/Tetris/Features#Lock_delay
"""
if self._applying_lock_delay:
self._applying_lock_delay = self._apply_lock(elapsed_time, well, stack, clear_current_piece)
self._applying_drop_delay = not self._applying_lock_delay
def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None, ghost_piece_off: bool = False) -> None:
# ghost piece
if well is not None and stack is not None and not ghost_piece_off:
ghost_piece_points = self._get_ghost_piece_points(well, stack)
if ghost_piece_points is not None:
for square in ghost_piece_points:
pygame.draw.polygon(surface, pygame.Color(self._ghost_piece_base_color), square, max(self._tile_size // 6, 1)) # TODO add white to the yaml
for square in self._points:
if self._base_color is not None:
square_color = pygame.Color(ConfigurationManager.get("color", self._base_color))
pygame.draw.polygon(surface, square_color, square, 0)
if self._outer_color is not None:
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", self._outer_color)), square, max(self._tile_size // 6, 1))
for square in self._points:
# inner color border piece
if self._inner_color:
vertex_one = (square[0][0] + (self._tile_size // 10), square[0][1] + (self._tile_size // 10))
vertex_two = (square[1][0] - (self._tile_size // 10), square[1][1] + (self._tile_size // 10))
vertex_three = (square[2][0] - (self._tile_size // 10), square[2][1] - (self._tile_size // 10))
vertex_four = (square[3][0] + (self._tile_size // 10), square[3][1] - (self._tile_size // 10))
new_square = (vertex_one, vertex_two, vertex_three, vertex_four)
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", self._inner_color)), new_square, max(self._tile_size // 6, 1))
# draw glimmer
surface.set_at((square[0][0]+3, square[0][1]+3), "white")
surface.set_at((square[0][0]+4, square[0][1]+4), "white")
surface.set_at((square[0][0]+5, square[0][1]+5), "white")
surface.set_at((square[0][0]+4, square[0][1]+5), "white")
surface.set_at((square[0][0]+5, square[0][1]+4), "white")
def move(self, vector: Tuple) -> None:
self._previous_points = copy.deepcopy(self._points)
self._previous_center = copy.deepcopy(self._center)
self._center[0] += vector[0]
self._center[1] += vector[1]
for square in self._points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
def rotate_with_wall_kick(self, well: Well, stack: "Stack") -> bool:
self._rotate()
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
# trying kick to the left
self.move((-self._tile_size, 0))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick to the left
return True
# trying kick to the right
self.move((self._tile_size, 0))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick to the right
return True
# trying kick from top
self.move((0, self._tile_size))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick from top
return True
# trying kick 2 tiles from top
self.move((0, self._tile_size * 2))
self._rotate(True)
if well and self.collide(well) or stack and self.collide(stack):
self.revert()
else:
# successful kick 2 tiles from top
return True
else:
return True
# failed rotation
return False
def revert(self) -> None:
if self._previous_points and self._previous_center:
self._points = self._previous_points
self._center = self._previous_center
'''
For more information on a rotation of a piece go here:
https://gamedev.stackexchange.com/questions/17974/how-to-rotate-blocks-in-tetris
'''
def _rotate(self, no_revert: bool = False) -> None:
if not no_revert:
self._previous_points = copy.deepcopy(self._points)
self._previous_center = copy.deepcopy(self._center)
new_points = []
for square in self._points:
for vertex in square:
h = vertex[0] - self._center[0]
k = vertex[1] - self._center[1]
vertex[0] = (k * -1) + self._center[0]
vertex[1] = h + self._center[1]
new_points.append([square[-1]] + square[0:-1])
self._points = new_points
@classmethod
def _get_points(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
points = []
for square in shape[:-1]:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
def _get_center(self, shape: Tuple, position: Tuple) -> List:
center = shape[-1]
return [int(center[0] * self._tile_size + position[0]), int(center[1] * self._tile_size + position[1])]
def _apply_drop(self, elapsed_time: int, well: Well, stack: "Stack") -> bool:
self._drop_delay_acc += elapsed_time
if self._drop_delay_acc >= self._drop_delay:
self._drop_delay_acc = 0
if not self._entity_is_below(well) and not self._entity_is_below(stack):
self.move((0, self._tile_size))
else:
return False
return True
def _apply_lock(self, elapsed_time: int, well: Well, stack: "Stack", clear_current_piece: FunctionType) -> bool:
self._lock_delay_acc += elapsed_time
if self._lock_delay_acc >= self._lock_delay:
self._add_to_stack(stack, clear_current_piece)
return False
return self._entity_is_below(well) or self._entity_is_below(stack)
def _add_to_stack(self, stack: "Stack", clear_current_piece: FunctionType) -> None:
SoundManager.play_piece_set_sfx()
stack.add_piece(self)
clear_current_piece()
def _mimic_move(self, vector: Tuple) -> List:
mimic_points = copy.deepcopy(self._points)
for square in mimic_points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
return mimic_points
def _entity_is_below(self, entity: Entity) -> bool:
mimic_points = self._mimic_move((0, self._tile_size))
return entity and entity._collide(mimic_points)
def _get_ghost_piece_points(self, well: Well, stack: "Stack") -> List:
current_points = copy.deepcopy(self._points)
prior_points = None
while not well._collide(current_points) and not stack._collide(current_points):
prior_points = copy.deepcopy(current_points)
for square in current_points:
for vertex in square:
vertex[1] += 1 * self._tile_size
return prior_points
# shape attributes
I_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((3, 0), (4, 0), (4, 1), (3, 1)), (2, 0))
J_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
L_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1.5, 0.5))
O_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1, 1))
S_SHAPE = (((0, 1), (1, 1), (1, 2), (0, 2)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
T_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
Z_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
"""
TODO description
"""
class Stack(Entity):
def __init__(self):
super().__init__([], None, None)
self._square_colors = []
self.total_lines = 0
self.lines_completed_last = 0
def update(self, elapsed_time: int) -> None:
super().update(elapsed_time)
self.lines_completed_last = self._complete_rows()
self.total_lines += self.lines_completed_last
def draw(self, surface: pygame.Surface) -> None:
# only draw if squares and colors are aligned
if len(self._square_colors) != len(self._points):
return
for i in range(len(self._points)):
square = self._points[i]
square_design = self._square_colors[i]
if square_design.base_color is not None:
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", square_design.base_color)), square, 0)
if square_design.outer_color is not None:
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", square_design.outer_color)), square, max(self._tile_size // 6, 1))
if square_design.inner_color is not None:
vertex_one = (square[0][0] + (self._tile_size // 10), square[0][1] + (self._tile_size // 10))
vertex_two = (square[1][0] - (self._tile_size // 10), square[1][1] + (self._tile_size // 10))
vertex_three = (square[2][0] - (self._tile_size // 10), square[2][1] - (self._tile_size // 10))
vertex_four = (square[3][0] + (self._tile_size // 10), square[3][1] - (self._tile_size // 10))
new_square = (vertex_one, vertex_two, vertex_three, vertex_four)
pygame.draw.polygon(surface, pygame.Color(ConfigurationManager.get("color", square_design.inner_color)), new_square, max(self._tile_size // 6, 1))
# draw glimmer
surface.set_at((square[0][0]+3, square[0][1]+3), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+4), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+5, square[0][1]+5), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+4, square[0][1]+5), pygame.Color(255, 255, 255))
surface.set_at((square[0][0]+5, square[0][1]+4), pygame.Color(255, 255, 255))
def add_piece(self, piece: Piece) -> None:
self._points += piece._points
self._square_colors += [SquareColor(piece._base_color, piece._inner_color, piece._outer_color) for _ in range(len(piece._points))]
# TODO refactor into multiple functions
def _complete_rows(self) -> int:
squares_by_row = {}
for square in self._points:
top_left_vertex = square[0]
if top_left_vertex[1] not in squares_by_row:
squares_by_row[top_left_vertex[1]] = []
if square not in squares_by_row[top_left_vertex[1]]:
squares_by_row[top_left_vertex[1]].append(square)
squares_to_exclude = []
rows_completed = []
for key, value in squares_by_row.items():
if len(squares_by_row[key]) == Well.WIDTH:
squares_to_exclude += value
rows_completed.append(key)
if not squares_to_exclude:
return 0
if len(rows_completed) == 4:
SoundManager.play_four_lines_complete_sfx()
else:
SoundManager.play_line_complete_sfx()
new_points = []
self.new_square_colors = []
for i in range(len(self._points)):
square = self._points[i]
if square not in squares_to_exclude:
for vertex in square:
distance_to_move = sum(
vertex[1] <= row_completed
for row_completed in rows_completed
)
vertex[1] += self._tile_size * distance_to_move
new_points.append(square)
self.new_square_colors.append(self._square_colors[i])
self._points = new_points
self._square_colors = self.new_square_colors
return len(rows_completed)
"""
TODO description
"""
class SquareColor:
def __init__(self, base_color: str, inner_color: str, outer_color: str):
self.base_color = base_color
self.inner_color = inner_color
self.outer_color = outer_color
"""
TODO description
"""
class PieceGenerator:
_bucket = []
@classmethod
def get_piece(cls, position: Tuple) -> Piece:
if len(cls._bucket) == 0:
cls._generate_bucket(cls._bucket)
base_color, inner_color, outer_color = cls._get_piece_color()
return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_color, outer_color)
@classmethod
def get_opponent_piece(cls, base_color: str, inner_color: str, outer_color: str) -> Piece:
return Piece(Piece.Z_SHAPE, (-250, -250), base_color, inner_color, outer_color)
@classmethod
def _generate_bucket(cls, bucket: List) -> None:
piece_types = list(range(7))
while len(bucket) != 7:
random_number = random.randint(0, 6 - len(bucket))
bucket.append(piece_types.pop(random_number))
@classmethod
def _get_piece_shape(cls, piece_number: int) -> Tuple:
if piece_number == 0:
return Piece.I_SHAPE
if piece_number == 1:
return Piece.J_SHAPE
if piece_number == 2:
return Piece.L_SHAPE
if piece_number == 3:
return Piece.O_SHAPE
if piece_number == 4:
return Piece.S_SHAPE
if piece_number == 5:
return Piece.T_SHAPE
if piece_number == 6:
return Piece.Z_SHAPE
return None
@classmethod
def _get_piece_color(cls) -> Tuple:
random_number = random.randint(1, 3)
base_color = "piece-" + str(random_number) + "-player-1"
inner_color = None if random_number != 3 else "piece-inner-border-1-player-1"
outer_color = "piece-outer-border-1"
return (base_color, inner_color, outer_color)

62
tetri5/game.py Normal file
View File

@@ -0,0 +1,62 @@
import sys
from tetri5.online import MultiplayerService
import pygame
from tetri5.util import ConfigurationManager
from tetri5.util import TextGenerator
from tetri5.util import SoundManager
from tetri5.scene import Scene, TitleScene
# TODO improve game assets https://www.spriters-resource.com/nes/tetris/
# TODO create a util that manages sfx
class Game:
_current_scene = None
@classmethod
def init(cls) -> None:
pygame.init()
SoundManager.init()
TextGenerator.init(ConfigurationManager.get("image", "font"), (20, 20))
cls._current_scene = TitleScene(Game.change_scene)
win_width = ConfigurationManager.get("window", "width")
win_height = ConfigurationManager.get("window", "height")
win_title = ConfigurationManager.get("window", "title")
win_icon = ConfigurationManager.get("image", "window-icon")
cls.fps = ConfigurationManager.get("engine", "fps")
cls.tile_size = ConfigurationManager.get("engine", "tile-size")
cls.screen = pygame.display.set_mode((win_width, win_height))
cls.clock = pygame.time.Clock()
pygame.display.set_caption(win_title)
pygame.display.set_icon(pygame.image.load(win_icon))
@classmethod
def update(cls) -> None:
# TODO write not initialized exception
elapsed_time = cls.clock.tick(cls.fps)
if cls._current_scene:
cls._current_scene.update(elapsed_time)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
MultiplayerService.quit()
sys.exit()
@classmethod
def draw(cls) -> None:
# TODO write not initialized exception
if cls._current_scene:
cls._current_scene.draw(cls.screen)
# update display
pygame.display.update()
@classmethod
def change_scene(cls, scene: Scene) -> None:
cls._current_scene = scene

27
tetri5/modal.py Normal file
View File

@@ -0,0 +1,27 @@
import pygame
from pygame import Surface
from tetri5.util import SoundManager, TextGenerator
class GameOverModal:
def __init__(self):
self.is_open = False
self.is_winner = False
self.music_played = False
def show(self, is_winner: bool) -> None:
self.is_open = True
self.is_winner = is_winner
def draw(self, surface: Surface) -> None:
pygame.draw.rect(surface, "black", pygame.Rect(290, 240, 220, 100))
pygame.draw.rect(surface, "white", pygame.Rect(290, 240, 220, 100), 3, 5)
TextGenerator.draw("YOU LOSE" if not self.is_winner else "YOU WIN!", (320, 260), surface)
TextGenerator.draw("GAME OVER", (310, 300), surface)
def update(self, _: int) -> None:
if not self.music_played:
if self.is_winner:
SoundManager.play_you_win_sfx()
else:
SoundManager.play_game_over_sfx()
self.music_played = True

285
tetri5/online.py Normal file
View File

@@ -0,0 +1,285 @@
import asyncio
import websockets
import json
import queue # refer to https://docs.python.org/3/library/queue.html for cross threading communication
import uuid
from typing import Dict, List
from threading import Thread
from tetri5.util import ConfigurationManager
class MultiplayerService():
_thread = None
_current_game_id = None
_client_id = str(uuid.uuid4())
""" QUEUES """
_receive_piece_queue = queue.Queue()
_send_piece_queue = queue.Queue()
_receive_stack_queue = queue.Queue()
_send_stack_queue = queue.Queue()
_receive_stats_queue = queue.Queue()
_send_stats_queue = queue.Queue()
_receive_message_queue = queue.Queue()
_send_message_queue = queue.Queue()
WAIT_FOR_OPPONENT = "wait_for_opponent"
START_GAME = "start_game"
@classmethod
def init(cls) -> None:
thread = Thread(target=asyncio.get_event_loop().run_until_complete,\
args=(_NetworkConnectionService.init(),))
thread.start()
""" SEND """
@classmethod
def send_piece(cls, piece: "PieceDto") -> None:
cls._send_piece_queue.put(piece)
@classmethod
def send_stack(cls, stack: "StackDto") -> None:
cls._send_stack_queue.put(stack)
@classmethod
def send_stats(cls, stats: "StatsDto") -> None:
cls._send_stats_queue.put(stats)
@classmethod
def send_message(cls, message: str) -> None:
cls._send_message_queue.put(message)
""" RECEIVE """
@classmethod
def try_receive_piece(cls) -> "PieceDto":
if cls._receive_piece_queue.empty():
return None
result = cls._receive_piece_queue.get()
cls._receive_piece_queue.task_done()
return result
@classmethod
def try_receive_stack(cls) -> "StackDto":
if cls._receive_stack_queue.empty():
return None
result = cls._receive_stack_queue.get()
cls._receive_stack_queue.task_done()
return result
@classmethod
def try_receive_stats(cls) -> "StatsDto":
if cls._receive_stats_queue.empty():
return None
result = cls._receive_stats_queue.get()
cls._receive_stats_queue.task_done()
return result
@classmethod
def try_receive_message(cls) -> str:
if cls._receive_message_queue.empty():
return None
result = cls._receive_message_queue.get()
cls._receive_message_queue.task_done()
return result
""" MISC """
@classmethod
def enter_game(cls, game_id: str) -> None:
cls._current_game_id = game_id
_NetworkConnectionService._join_game = True # TODO: change this to a function
@classmethod
def quit(cls) -> None:
_NetworkConnectionService.close_connection()
class _NetworkConnectionService():
_websocket = None
_is_closed = False
_join_game = False
_pending_receive_task = None
@classmethod
async def init(cls) -> None:
await cls._connect_to_server()
await cls._run_network_loop()
""" NETWORK SEND """
@classmethod
async def _try_enter_game(cls) -> None:
if not cls._join_game:
return
json_message = json.dumps({"action": "enter_game",\
"clientId": MultiplayerService._client_id,\
"gameId": MultiplayerService._current_game_id})
await cls._websocket.send(json_message)
cls._join_game = False
@classmethod
async def _try_send_piece(cls) -> None:
# if no messages to proccess, return
if MultiplayerService._send_piece_queue.empty():
return
# get next piece to send and send to server
piece = MultiplayerService._send_piece_queue.get()
# construct json message
json_message = json.dumps({"action": "send_piece",\
"clientId": MultiplayerService._client_id,\
"gameId": MultiplayerService._current_game_id,\
"piece": piece.__dict__})
await cls._websocket.send(json_message)
MultiplayerService._send_piece_queue.task_done()
@classmethod
async def _try_send_stack(cls) -> None:
# if no messages to proccess, return
if MultiplayerService._send_stack_queue.empty():
return
# get next stack to send and send to server
stack = MultiplayerService._send_stack_queue.get()
# construct json message
json_message = json.dumps({"action": "send_stack",\
"clientId": MultiplayerService._client_id,\
"gameId": MultiplayerService._current_game_id,\
"stack": stack.__dict__})
await cls._websocket.send(json_message)
MultiplayerService._send_stack_queue.task_done()
@classmethod
async def _try_send_stats(cls) -> None:
# if no messages to proccess, return
if MultiplayerService._send_stats_queue.empty():
return
# get next stats to send and send to server
stats = MultiplayerService._send_stats_queue.get()
# construct json message
json_message = json.dumps({"action": "send_stats",\
"clientId": MultiplayerService._client_id,\
"gameId": MultiplayerService._current_game_id,\
"stats": stats.__dict__})
await cls._websocket.send(json_message)
MultiplayerService._send_stats_queue.task_done()
@classmethod
async def _try_send_message(cls) -> None:
# if no messages to proccess, return
if MultiplayerService._send_message_queue.empty():
return
# get next message to send and send to server
message = MultiplayerService._send_message_queue.get()
await cls._websocket.send(message)
MultiplayerService._send_message_queue.task_done()
""" NETWORK RECEIVE """
# todo refactor
@classmethod
async def _try_receive_message(cls) -> None:
try:
task = cls._pending_receive_task or asyncio.create_task(cls._websocket.recv())
done, pending = await asyncio.wait({task}, timeout=2e-3) # TODO experiment with the timeout
if task in done:
json_str = await task
if json_str == "pong":
print("pong")
cls._pending_receive_task = None
return
data = json.loads(json_str)
if data["type"] == "wait_for_opponent":
MultiplayerService._receive_message_queue.put(MultiplayerService.WAIT_FOR_OPPONENT)
if data["type"] == "start_game":
MultiplayerService._receive_message_queue.put(MultiplayerService.START_GAME)
if data["type"] == "receive_piece":
MultiplayerService._receive_piece_queue.put(PieceDto.create(data["piece"]))
if data["type"] == "receive_stack":
MultiplayerService._receive_stack_queue.put(StackDto.create(data["stack"]))
if data["type"] == "receive_stats":
MultiplayerService._receive_stats_queue.put(StatsDto.create(data["stats"]))
if data["type"] == "exit_game":
print("Exit the game!")
cls.close_connection()
cls._pending_receive_task = None
elif len(pending):
cls._pending_receive_task = pending.pop()
finally:
pass # TODO handle connection closed exception and attempt to reconnect
""" MISC """
@classmethod
async def _run_network_loop(cls) -> None:
while True:
await asyncio.sleep(2e-3) # TODO add clock tick instead
await cls._try_enter_game()
await cls._try_send_piece()
await cls._try_send_stack()
await cls._try_send_stats()
await cls._try_send_message()
await cls._try_receive_message()
# if conection is closed, exit loop
if cls._is_closed:
await cls._websocket.close()
break
# ping_interval=None is important, otherwise the server will disconnect us
# https://stackoverflow.com/a/58993145/11512104
@classmethod
async def _connect_to_server(cls) -> None:
print("Connecting to server...") # TODO replace with logging
url = ConfigurationManager.get("online", "server-url")
cls._websocket = await websockets.connect(url, ping_interval=None)
print("Connected to server...") # TODO replace with logging
@classmethod
def close_connection(cls) -> None:
cls._is_closed = True
# DTOs
class PieceDto():
def __init__(self, points: List, center: List, base_color: str, inner_color: str, outer_color: str) -> None:
self.points = points
self.center = center
self.base_color = base_color
self.inner_color = inner_color
self.outer_color = outer_color
@staticmethod
def create(data: Dict) -> "PieceDto":
return PieceDto(data["points"], data["center"], data["base_color"], data["inner_color"], data["outer_color"])
class StackDto():
def __init__(self, points: List, square_colors: List[Dict]) -> None:
self.points = points
self.square_colors = square_colors
@staticmethod
def create(data: Dict) -> "StackDto":
return StackDto(data["points"], data["square_colors"])
class StatsDto():
def __init__(self, score: int, lines: int, is_well_full: bool) -> None:
self.score = score
self.lines = lines
self.is_well_full = is_well_full
@staticmethod
def create(data: Dict) -> "StatsDto":
return StatsDto(data["score"], data["lines"], data["is_well_full"])

479
tetri5/scene.py Normal file
View File

@@ -0,0 +1,479 @@
import pygame
from types import FunctionType
from tetri5.util import ConfigurationManager
from tetri5.util import TextGenerator
from tetri5.util import Controller
from tetri5.util import SoundManager
from tetri5.entity import SquareColor, Well
from tetri5.entity import Stack
from tetri5.entity import PieceGenerator
from tetri5.online import *
from tetri5.modal import GameOverModal
"""
TODO
"""
class Scene:
pass
"""
TODO
"""
class TitleScene(Scene):
# Title screen options
ONE_PLAYER = "1 PLAYER"
TWO_PLAYER = "2 PLAYER"
def __init__(self, change_scene: FunctionType) -> None:
self._tile_size = ConfigurationManager.get("engine", "tile-size")
self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
self._logo_image = pygame.image.load(ConfigurationManager.get("image", "title-screen"))
self._cursor_pos_one = ConfigurationManager.get("position", "cursor-option-one")
self._cursor_pos_two = ConfigurationManager.get("position", "cursor-option-two")
self._cursor_pos = self._cursor_pos_one
self._cursor_blink_interval = ConfigurationManager.get("engine", "cursor-blink-interval")
self._cursor_blink_time = 0
self._cursor_color = pygame.Color(ConfigurationManager.get("color", "cursor"))
self._cursor_off = False
self._logo_pos = ConfigurationManager.get("position", "title-logo")
self._option_one_pos = ConfigurationManager.get("position", "option-one")
self._option_two_pos = ConfigurationManager.get("position", "option-two")
self._is_multiplayer = False
self._change_scence = change_scene
def draw(self, surface: pygame.Surface) -> None:
surface.fill(self._background_color)
surface.blit(self._logo_image, self._logo_pos)
TextGenerator.draw(TitleScene.ONE_PLAYER, self._option_one_pos, surface)
TextGenerator.draw(TitleScene.TWO_PLAYER, self._option_two_pos, surface)
if self._cursor_off:
pygame.draw.circle(surface, self._cursor_color, self._cursor_pos, self._tile_size // 3)
def update(self, elapsed_time: int) -> None:
option_change = False
if Controller.key_pressed(pygame.K_UP) and self._is_multiplayer:
self._cursor_pos = self._cursor_pos_one
self._is_multiplayer = False
option_change = True
if Controller.key_pressed(pygame.K_DOWN) and not self._is_multiplayer:
self._cursor_pos = self._cursor_pos_two
self._is_multiplayer = True
option_change = True
if option_change:
SoundManager.play_option_change_sfx() # TODO add cool down
self._cursor_blink_time += elapsed_time
if self._cursor_blink_time >= self._cursor_blink_interval:
self._cursor_blink_time = 0
self._cursor_off = not self._cursor_off
if Controller.key_pressed(pygame.K_RETURN):
self._change_scence(SinglePlayerScene(self._change_scence) if not self._is_multiplayer else ConnectionScene(self._change_scence))
"""
TODO
"""
class SinglePlayerScene(Scene):
def __init__(self, change_scene: FunctionType) -> None:
self._top_label_pos = ConfigurationManager.get("position", "top-label")
self._top_value_pos = ConfigurationManager.get("position", "top-value")
self._score_label_pos = ConfigurationManager.get("position", "score-label")
self._score_value_pos = ConfigurationManager.get("position", "score-value")
self._lines_label_pos = ConfigurationManager.get("position", "lines-label")
self._next_label_pos = ConfigurationManager.get("position", "next-label")
self._level_label_pos = ConfigurationManager.get("position", "level-label")
self._next_piece_pos = ConfigurationManager.get("position", "next-piece")
self._spawn_piece_shift = ConfigurationManager.get("position", "spawn-piece")
self._tile_size = ConfigurationManager.get("engine", "tile-size")
self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
self._points_table = ConfigurationManager.get("engine", "points-table")
self._lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
self._score = 0
self._previous_level = 0
self._current_piece = None
self._next_piece = PieceGenerator.get_piece(self._next_piece_pos)
self._well = Well(ConfigurationManager.get("position", "well"),\
ConfigurationManager.get("color", "well-1"),\
ConfigurationManager.get("color", "well-border-1"))
self._stack = Stack()
self._game_over_modal = GameOverModal()
self._change_scence = change_scene
SoundManager.play_theme_music_single()
def draw(self, surface: pygame.Surface) -> None:
if self._game_over_modal.is_open:
self._game_over_modal.draw(surface)
return
surface.fill(self._background_color)
if self._stack:
self._stack.draw(surface)
if self._current_piece:
self._current_piece.draw(surface, self._well, self._stack)
if self._next_piece:
self._next_piece.draw(surface)
if self._well:
self._well.draw(surface)
score = str(self._score).zfill(6)
lines = str(self._stack.total_lines).zfill(4)
level = str(self._get_level()).zfill(2)
TextGenerator.draw("Top", self._top_label_pos, surface)
TextGenerator.draw("000000", self._top_value_pos, surface)
TextGenerator.draw("Score", self._score_label_pos, surface)
TextGenerator.draw(score, self._score_value_pos, surface)
TextGenerator.draw("Lines " + lines, self._lines_label_pos, surface)
TextGenerator.draw("Next", self._next_label_pos, surface)
TextGenerator.draw("LVL " + level, self._level_label_pos, surface)
def update(self, elapsed_time: int) -> None:
if self._game_over_modal.is_open:
self._game_over_modal.update(elapsed_time)
return
if self._current_piece:
self._current_piece.update(elapsed_time,\
self._well,\
self._stack,\
self._get_level(),\
self._clear_current_piece)
else:
self._current_piece = self._next_piece
self._current_piece.move(self._spawn_piece_shift)
self._next_piece = PieceGenerator.get_piece(self._next_piece_pos)
# TODO create game over scene
if self._stack and self._current_piece.collide(self._stack):
SoundManager.stop_theme_music_single()
self._game_over_modal.show(False)
if self._stack:
self._stack.update(elapsed_time)
self._score += self._points_table[self._stack.lines_completed_last] * (self._get_level() + 1)
if self._previous_level != self._get_level():
self._previous_level = self._get_level()
SoundManager.play_level_up_sfx()
def _get_level(self) -> int:
return 0 if self._stack is None else self._stack.total_lines // self._lines_per_level
def _clear_current_piece(self) -> None:
self._current_piece = None
class ConnectionScene(Scene):
MAX_GAME_ID_LEN = 5
MAX_PERIOD_COUNT = 3
def __init__(self, change_scene: FunctionType) -> None:
self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
self._is_connecting = False
self._waiting_for_opponent = False
self._game_id_label_pos = ConfigurationManager.get("position", "game-id-label")
self._game_id_label = "Enter Game Id: "
self._game_id = "_"
self._cursor_blink_interval = ConfigurationManager.get("engine", "cursor-blink-interval")
self._cursor_blink_acc = 0 # blink accumulator
self._connecting_label_pos = ConfigurationManager.get("position", "connecting-label")
self._connecting_label = "Connecting"
self._waiting_for_opponent_label_pos = ConfigurationManager.get("position", "waiting-for-opponent-label")
self._waiting_for_opponent_label = "Waiting for opponent"
self._period_blink_interval = ConfigurationManager.get("engine", "period-blink-interval")
self._period_blink_acc = 0 # period accumulator
self._ping_interval = ConfigurationManager.get("engine", "ping-interval")
self._ping_acc = 0 # ping accumulator
self._change_scene = change_scene
def draw(self, surface: pygame.Surface) -> None:
surface.fill(self._background_color)
if not self._is_connecting and not self._waiting_for_opponent:
TextGenerator.draw(self._game_id_label + self._game_id, self._game_id_label_pos, surface)
elif self._is_connecting and not self._waiting_for_opponent:
TextGenerator.draw(self._connecting_label, self._connecting_label_pos, surface)
else:
TextGenerator.draw(self._waiting_for_opponent_label, self._waiting_for_opponent_label_pos, surface)
def update(self, elapsed_time: int) -> None:
# cursor blink logic
self._cursor_blink_acc += elapsed_time
if self._cursor_blink_acc >= self._cursor_blink_interval:
self._cursor_blink_acc = 0
if self._game_id.find("_") != -1:
self._game_id = self._game_id.replace("_", "")
else:
self._game_id += "_"
# period ellipsis logic for connecting
if self._is_connecting and not self._waiting_for_opponent:
self._period_blink_acc += elapsed_time
if self._period_blink_acc >= self._period_blink_interval:
self._period_blink_acc = 0
period_count = self._connecting_label.count(".")
if period_count < self.MAX_PERIOD_COUNT:
self._connecting_label += "."
else:
self._connecting_label = self._connecting_label.replace(".", "")
# period ellipsis logic for waiting on opponent
if self._waiting_for_opponent:
self._period_blink_acc += elapsed_time
if self._period_blink_acc >= self._period_blink_interval:
self._period_blink_acc = 0
period_count = self._waiting_for_opponent_label.count(".")
if period_count < self.MAX_PERIOD_COUNT:
self._waiting_for_opponent_label += "."
else:
self._waiting_for_opponent_label = self._waiting_for_opponent_label.replace(".", "")
# keyboard input
for event in pygame.event.get(pygame.KEYDOWN):
# user input logic
if not self._is_connecting:
self._game_id = self._game_id.replace("_", "")
if event.key == pygame.K_BACKSPACE:
self._game_id = self._game_id[:-1]
elif (event.unicode.isalpha() or event.unicode.isdigit()) and\
len(self._game_id) <= self.MAX_GAME_ID_LEN:
self._game_id += event.unicode.upper()
# connection logic
if event.key == pygame.K_RETURN and len(self._game_id) > 0 and not self._is_connecting:
self._is_connecting = True
MultiplayerService.init()
MultiplayerService.enter_game(self._game_id)
# server messaging logic
self._ping_acc += elapsed_time
message = MultiplayerService.try_receive_message();
if message: # TODO remove later, only for testing
print(message)
if message == MultiplayerService.WAIT_FOR_OPPONENT:
self._waiting_for_opponent = True
if message == MultiplayerService.START_GAME:
self._change_scene(MultiplayerScene(self._change_scene))
if message is None and self._ping_acc >= self._ping_interval:
self._ping_acc = 0
MultiplayerService.send_message("ping")
"""
TODO
"""
class MultiplayerScene(Scene):
def __init__(self, change_scene: FunctionType) -> None:
self._tile_size = ConfigurationManager.get("engine", "tile-size")
self._background_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
self._lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
self._points_table = ConfigurationManager.get("engine", "points-table")
self._score_player_one = 0
self._score_player_two = 0
self._total_lines_player_two = 0
# wells init
self._well_player_one = Well(ConfigurationManager.get("position", "well-player-1"),\
ConfigurationManager.get("color", "well-1"),\
ConfigurationManager.get("color", "well-border-1"))
self._well_player_two = Well(ConfigurationManager.get("position", "well-player-2"),\
ConfigurationManager.get("color", "well-1"),\
ConfigurationManager.get("color", "well-border-1"))
# stacks init
self._stack_player_one = Stack()
self._stack_player_two = Stack()
# score positions
self._score_label_player_one_pos = ConfigurationManager.get("position", "score-label-player-1")
self._score_label_player_two_pos = ConfigurationManager.get("position", "score-label-player-2")
self._score_value_player_one_pos = ConfigurationManager.get("position", "score-value-player-1")
self._score_value_player_two_pos = ConfigurationManager.get("position", "score-value-player-2")
# lines positions
self._lines_label_player_one_pos = ConfigurationManager.get("position", "lines-label-player-1")
self._lines_label_player_two_pos = ConfigurationManager.get("position", "lines-label-player-2")
# next positions
self._next_label_player_one_pos = ConfigurationManager.get("position", "next-label-player-1")
# piece positions
self._next_piece_player_one_pos = ConfigurationManager.get("position", "next-piece-player-1")
self._spawn_piece_shift_player_one = ConfigurationManager.get("position", "spawn-piece-player-1")
self._spawn_piece_shift_player_two = ConfigurationManager.get("position", "spawn-piece-player-2")
# entities
self._next_piece_player_one = PieceGenerator.get_piece(self._next_piece_player_one_pos)
self._current_piece_player_one = None
self._current_piece_player_two = None
self._game_over_modal = GameOverModal()
self._well_full = False
self._change_scence = change_scene
SoundManager.play_theme_music_multi()
def draw(self, surface: pygame.Surface) -> None:
if self._game_over_modal.is_open:
self._game_over_modal.draw(surface)
return
surface.fill(self._background_color)
# stacks
if self._stack_player_one is not None:
self._stack_player_one.draw(surface)
if self._stack_player_two is not None:
self._stack_player_two.draw(surface)
# pieces
if self._current_piece_player_one is not None:
self._current_piece_player_one.draw(surface, self._well_player_one, self._stack_player_one)
if self._current_piece_player_two is not None:
self._current_piece_player_two.draw(surface, self._well_player_two, self._stack_player_two, True)
if self._next_piece_player_one is not None:
self._next_piece_player_one.draw(surface)
# wells
if self._well_player_one is not None:
self._well_player_one.draw(surface)
if self._well_player_two is not None:
self._well_player_two.draw(surface)
score_player_one = str(self._score_player_one).zfill(6)
score_player_two = str(self._score_player_two).zfill(6)
lines_player_one = str(self._stack_player_one.total_lines).zfill(4)
lines_player_two = str(self._total_lines_player_two).zfill(4)
# scores
TextGenerator.draw("Score", self._score_label_player_one_pos, surface)
TextGenerator.draw(score_player_one, self._score_value_player_one_pos, surface)
TextGenerator.draw("Score", self._score_label_player_two_pos, surface)
TextGenerator.draw(score_player_two, self._score_value_player_two_pos, surface)
# lines
TextGenerator.draw("Lines " + lines_player_one, self._lines_label_player_one_pos, surface)
TextGenerator.draw("Lines " + lines_player_two, self._lines_label_player_two_pos, surface)
# next
TextGenerator.draw("NEXT", self._next_label_player_one_pos, surface)
def update(self, elapsed_time: int) -> None:
if self._game_over_modal.is_open:
self._game_over_modal.update(elapsed_time)
return
self._update_piece_player_one(elapsed_time)
self._update_piece_player_two()
if self._stack_player_one is not None:
self._stack_player_one.update(elapsed_time)
self._update_stack_player_two()
self._score_player_one += self._points_table[self._stack_player_one.lines_completed_last] * (self._get_level_player_one() + 1)
opponent_stats = MultiplayerService.try_receive_stats()
if opponent_stats is not None:
self._score_player_two = opponent_stats.score
self._total_lines_player_two = opponent_stats.lines
if opponent_stats.is_well_full:
SoundManager.stop_theme_music_multi()
self._game_over_modal.show(True)
if self._current_piece_player_one is not None:
MultiplayerService.send_piece(PieceDto(self._current_piece_player_one._points,\
self._current_piece_player_one._center,\
self._current_piece_player_one._base_color,\
self._current_piece_player_one._inner_color,\
self._current_piece_player_one._outer_color))
if self._stack_player_one is not None:
MultiplayerService.send_stack(
StackDto(
self._stack_player_one._points,
[
x.__dict__
for x in self._stack_player_one._square_colors
],
)
)
MultiplayerService.send_stats(StatsDto(self._score_player_one, self._stack_player_one.total_lines, self._well_full))
def _update_piece_player_one(self, elapsed_time: int) -> None:
if self._current_piece_player_one is not None:
self._current_piece_player_one.update(elapsed_time,\
self._well_player_one,\
self._stack_player_one,\
self._get_level_player_one(),\
self._clear_current_piece_player_one)
else:
self._current_piece_player_one = self._next_piece_player_one
self._current_piece_player_one.move(self._spawn_piece_shift_player_one)
self._next_piece_player_one = PieceGenerator.get_piece(self._next_piece_player_one_pos)
# TODO create game over scene
if self._stack_player_one and self._current_piece_player_one.collide(self._stack_player_one):
self._well_full = True
SoundManager.stop_theme_music_multi()
self._game_over_modal.show(False)
def _update_piece_player_two(self) -> None:
if self._current_piece_player_two is not None:
# get opponent piece from server and modify for current client
opponent_piece = MultiplayerService.try_receive_piece()
if opponent_piece is not None:
for square in opponent_piece.points:
for vertex in square:
vertex[0] += 400
opponent_piece.center[0] += 400
# load opponent piece into game
self._current_piece_player_two._points = opponent_piece.points
self._current_piece_player_two._center = opponent_piece.center
self._current_piece_player_two._base_color = opponent_piece.base_color.replace("player-1", "player-2")
if opponent_piece.inner_color is not None:
self._current_piece_player_two._inner_color = opponent_piece.inner_color.replace("player-1", "player-2")
else:
self._current_piece_player_two._inner_color = None
self._current_piece_player_two._outer_color = opponent_piece.outer_color.replace("player-1", "player-2")
else:
self._current_piece_player_two = PieceGenerator.get_opponent_piece(None, None, None)
def _update_stack_player_two(self) -> None:
if self._stack_player_two is not None:
# get opponent stack from server and modify for current client
opponent_stack = MultiplayerService.try_receive_stack()
if opponent_stack is not None:
for square in opponent_stack.points:
for vertex in square:
vertex[0] += 400
# load opponent stack into game
self._stack_player_two._points = opponent_stack.points
self._stack_player_two._square_colors = [
SquareColor(x["base_color"].replace("player-1", "player-2"),\
x["inner_color"].replace("player-1", "player-2") if x["inner_color"] else None,\
x["outer_color"].replace("player-1", "player-2"))
for x in opponent_stack.square_colors
]
def _get_level_player_one(self) -> int:
return 0 if self._stack_player_one is None else self._stack_player_one.total_lines // self._lines_per_level
def _get_level_player_two(self) -> int:
return 0 if self._stack_player_two is None else self._stack_player_two.total_lines // self._lines_per_level
def _clear_current_piece_player_one(self) -> None:
self._current_piece_player_one = None
def _clear_current_piece_player_two(self) -> None:
self._current_piece_player_two = None

208
tetri5/util.py Normal file
View File

@@ -0,0 +1,208 @@
import yaml
import pygame
from typing import Tuple
"""
TODO description
"""
class ConfigurationManager:
CONFIG_FILE_LOCATION = "config.yaml"
_configuration = []
@classmethod
def init(cls) -> None:
with open(cls.CONFIG_FILE_LOCATION, "r") as yaml_file:
cls._configuration = yaml.safe_load(yaml_file)
@classmethod
def get(cls, key: str, sub_key: str = None):
if sub_key:
return cls._configuration[key][sub_key]
else:
return cls._configuration[key]
"""
TODO description
"""
class SoundManager:
# Channels
_theme_music_ch = None
_option_change_sfx_ch = None
_piece_rotate_sfx_ch = None
_piece_set_sfx_ch = None
_line_complete_sfx_ch = None
_four_lines_complete_sfx_ch = None
# Sounds
_theme_music = None
_option_change_sfx = None
_piece_rotate_sfx = None
_piece_set_sfx = None
_line_complete_sfx = None
_four_lines_complete_sfx = None
@classmethod
def init(cls) -> None:
pygame.mixer.init()
cls._theme_music_single_ch = pygame.mixer.Channel(0)
cls._theme_music_multi_ch = pygame.mixer.Channel(1)
cls._option_change_sfx_ch = pygame.mixer.Channel(2)
cls._piece_rotate_sfx_ch = pygame.mixer.Channel(3)
cls._piece_set_sfx_ch = pygame.mixer.Channel(4)
cls._line_complete_sfx_ch = pygame.mixer.Channel(5)
cls._four_lines_complete_sfx_ch = pygame.mixer.Channel(6)
cls._level_up_sfx_ch = pygame.mixer.Channel(7)
cls._theme_music_single = pygame.mixer.Sound(ConfigurationManager.get("sound", "theme-music-single"))
cls._theme_music_multi = pygame.mixer.Sound(ConfigurationManager.get("sound", "theme-music-multi"))
cls._option_change_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "option-change"))
cls._piece_rotate_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "piece-rotate"))
cls._piece_set_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "piece-set"))
cls._line_complete_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "line-complete"))
cls._four_lines_complete_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "four-lines-complete"))
cls._level_up_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "level-up"))
cls._game_over_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "game-over"))
cls._you_win_sfx = pygame.mixer.Sound(ConfigurationManager.get("sound", "you-win"))
@classmethod
def play_theme_music_single(cls) -> None:
cls._theme_music_single_ch.set_volume(0.7)
cls._theme_music_single_ch.play(cls._theme_music_single, -1)
@classmethod
def stop_theme_music_single(cls) -> None:
cls._theme_music_single_ch.stop()
@classmethod
def play_theme_music_multi(cls) -> None:
cls._theme_music_multi_ch.set_volume(0.7)
cls._theme_music_multi_ch.play(cls._theme_music_multi, -1)
@classmethod
def stop_theme_music_multi(cls) -> None:
cls._theme_music_multi_ch.stop()
@classmethod
def play_option_change_sfx(cls) -> None:
cls._option_change_sfx_ch.set_volume(0.8)
cls._option_change_sfx_ch.play(cls._option_change_sfx)
@classmethod
def play_piece_rotate_sfx(cls) -> None:
cls._piece_rotate_sfx_ch.set_volume(0.7)
cls._piece_rotate_sfx_ch.play(cls._piece_rotate_sfx)
@classmethod
def play_piece_set_sfx(cls) -> None:
cls._piece_set_sfx_ch.play(cls._piece_set_sfx)
@classmethod
def play_line_complete_sfx(cls) -> None:
cls._line_complete_sfx_ch.play(cls._line_complete_sfx)
@classmethod
def play_four_lines_complete_sfx(cls) -> None:
cls._four_lines_complete_sfx_ch.play(cls._four_lines_complete_sfx)
@classmethod
def play_level_up_sfx(cls) -> None:
cls._level_up_sfx_ch.play(cls._level_up_sfx)
@classmethod
def play_game_over_sfx(cls) -> None:
cls._game_over_sfx.set_volume(0.7)
cls._game_over_sfx.play()
@classmethod
def play_you_win_sfx(cls) -> None:
cls._you_win_sfx.set_volume(0.7)
cls._you_win_sfx.play()
"""
TODO description
"""
class TextGenerator:
_sheet = None
_glyph_size = (-1, -1)
_characters = { }
@classmethod
def init(cls, file: str, glyph_size: Tuple) -> None:
cls._sheet = pygame.image.load(file)
cls._glyph_size = glyph_size
# load character positions in bitmap into the characters dictionary
# letters
cls._characters["A"] = (9 * glyph_size[0], 2 * glyph_size[1])
cls._characters["B"] = (10 * glyph_size[0], 2 * glyph_size[1])
cls._characters["C"] = (11 * glyph_size[0], 2 * glyph_size[1])
cls._characters["D"] = (0 * glyph_size[0], 3 * glyph_size[1])
cls._characters["E"] = (1 * glyph_size[0], 3 * glyph_size[1])
cls._characters["F"] = (2 * glyph_size[0], 3 * glyph_size[1])
cls._characters["G"] = (3 * glyph_size[0], 3 * glyph_size[1])
cls._characters["H"] = (4 * glyph_size[0], 3 * glyph_size[1])
cls._characters["I"] = (5 * glyph_size[0], 3 * glyph_size[1])
cls._characters["J"] = (6 * glyph_size[0], 3 * glyph_size[1])
cls._characters["K"] = (7 * glyph_size[0], 3 * glyph_size[1])
cls._characters["L"] = (8 * glyph_size[0], 3 * glyph_size[1])
cls._characters["M"] = (9 * glyph_size[0], 3 * glyph_size[1])
cls._characters["N"] = (10 * glyph_size[0], 3 * glyph_size[1])
cls._characters["O"] = (11 * glyph_size[0], 3 * glyph_size[1])
cls._characters["P"] = (0 * glyph_size[0], 4 * glyph_size[1])
cls._characters["Q"] = (1 * glyph_size[0], 4 * glyph_size[1])
cls._characters["R"] = (2 * glyph_size[0], 4 * glyph_size[1])
cls._characters["S"] = (3 * glyph_size[0], 4 * glyph_size[1])
cls._characters["T"] = (4 * glyph_size[0], 4 * glyph_size[1])
cls._characters["U"] = (5 * glyph_size[0], 4 * glyph_size[1])
cls._characters["V"] = (6 * glyph_size[0], 4 * glyph_size[1])
cls._characters["W"] = (7 * glyph_size[0], 4 * glyph_size[1])
cls._characters["X"] = (8 * glyph_size[0], 4 * glyph_size[1])
cls._characters["Y"] = (9 * glyph_size[0], 4 * glyph_size[1])
cls._characters["Z"] = (10 * glyph_size[0], 4 * glyph_size[1])
# numbers
cls._characters["0"] = (4 * glyph_size[0], 1 * glyph_size[1])
cls._characters["1"] = (5 * glyph_size[0], 1 * glyph_size[1])
cls._characters["2"] = (6 * glyph_size[0], 1 * glyph_size[1])
cls._characters["3"] = (7 * glyph_size[0], 1 * glyph_size[1])
cls._characters["4"] = (8 * glyph_size[0], 1 * glyph_size[1])
cls._characters["5"] = (9 * glyph_size[0], 1 * glyph_size[1])
cls._characters["6"] = (10 * glyph_size[0], 1 * glyph_size[1])
cls._characters["7"] = (11 * glyph_size[0], 1 * glyph_size[1])
cls._characters["8"] = (0 * glyph_size[0], 2 * glyph_size[1])
cls._characters["9"] = (1 * glyph_size[0], 2 * glyph_size[1])
# symbols
cls._characters[":"] = (2 * glyph_size[0], 11 * glyph_size[1])
cls._characters["_"] = (3 * glyph_size[0], 5 * glyph_size[1])
cls._characters["."] = (2 * glyph_size[0], 1 * glyph_size[1])
cls._characters["!"] = (1 * glyph_size[0], 0 * glyph_size[1])
@classmethod
def draw(cls, text: str, position: Tuple, surface: pygame.Surface) -> None:
x_position = 0
for char_ in text:
if not char_.isspace():
surface.blit(cls._sheet, (position[0] + x_position, position[1]), pygame.Rect(cls._characters[char_.upper()], (cls._glyph_size[0], cls._glyph_size[1])))
x_position += cls._glyph_size[0]
"""
TODO description
"""
class Controller:
_keys_pressed = {}
@classmethod
def key_down(cls, key: int) -> bool:
prior_pressed_state = False if key not in cls._keys_pressed else cls._keys_pressed[key]
cls._keys_pressed[key] = pygame.key.get_pressed()[key]
return cls._keys_pressed[key] and not prior_pressed_state
@classmethod
def key_pressed(cls, key: int) -> bool:
return pygame.key.get_pressed()[key]

View File

@@ -1,406 +0,0 @@
import copy
import random
import pygame
from typing import List, Tuple
from pygame import mixer
from tetris.util import ConfigurationManager
from tetris.util import Controller
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tetris.game import Game
"""
TODO description
"""
class Entity:
def __init__(self, points: Tuple, color: str, border_color: str = None):
self.points = points
self.color = color
self.border_color = border_color
self.elapsed_time = 0
def update(self, elapsed_time: int) -> None:
self.elapsed_time += elapsed_time
def draw(self, surface: pygame.Surface) -> None:
tile_size = ConfigurationManager.get("engine", "tile-size")
for square in self.points:
pygame.draw.polygon(surface, pygame.Color(self.color), square, 0)
if self.border_color:
pygame.draw.polygon(surface, pygame.Color(self.border_color), square, max(tile_size // 6, 1))
def collide(self, entity: "Entity") -> bool: # TODO figure out how to do type hint for entity param of type Entity
for square_one in self.points:
for square_two in entity.points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
def _collide(self, points: List) -> bool:
for square_one in self.points:
for square_two in points:
for i in range(4): # 4 vertices
if square_one[i][0] == square_two[i][0] and square_one[i][1] == square_two[i][1]:
return True
return False
"""
TODO description
"""
class Well(Entity):
WIDTH = 10 # standard tetris well width, should not be changed
HEIGHT = 20 # standard tetris well height, should not be changed
def __init__(self, position: Tuple, color: str, border_color: str):
super().__init__(self._get_points(position), color, border_color)
def _get_points(self, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
shape = []
for i in range(self.WIDTH + 2):
for j in range(self.HEIGHT + 2):
if i == 0 or i == self.WIDTH + 1:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
elif j == 0 or j == self.HEIGHT + 1:
shape.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)))
points = []
for square in shape:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
'''
For information on the Tetris piece Tetromino go here:
https://tetris.fandom.com/wiki/Tetromino
'''
class Piece(Entity):
def __init__(self, shape: Tuple, position: Tuple, color: str, inner_border_color: str, border_color: str):
super().__init__(self._get_points(shape, position), color, border_color)
self.inner_border_color = inner_border_color
self.center = self._get_center(shape, position)
self.piece_set_sound = mixer.Channel(2)
self.previous_points = None
self.previous_center = None
# Gravity
self.gravity_time = ConfigurationManager.get("engine", "piece-gravity-time")
self.current_gravity_time = 0
self.applying_gravity = True
# Set
self.set_time = ConfigurationManager.get("engine", "piece-set-time")
self.current_set_time = 0
self.applying_set = False
def update(self, elapsed_time: int, game: "Game") -> None:
super().update(elapsed_time)
tile_size = ConfigurationManager.get("engine", "tile-size")
if self.applying_gravity:
self.applying_gravity = self._apply_gravity(elapsed_time, game.well, game.stack)
self.applying_set = not self.applying_gravity
"""
For more information on the piece set logic go here:
https://strategywiki.org/wiki/Tetris/Features#Lock_delay
"""
if self.applying_set:
self.applying_set = self._apply_set(elapsed_time, game)
self.applying_gravity = not self.applying_set
# handle rotation, left and right movement
if Controller.key_down(pygame.K_SPACE):
self.rotate()
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
self.revert()
if Controller.key_down(pygame.K_LEFT):
self.move((-tile_size, 0))
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
self.revert()
if Controller.key_down(pygame.K_RIGHT):
self.move((tile_size, 0))
if game.well and self.collide(game.well) or game.stack and self.collide(game.stack):
self.revert()
# handle soft drop movement and gravity based on level
gravity_time = ConfigurationManager.get("engine", "piece-gravity-time")
set_time = ConfigurationManager.get("engine", "piece-set-time")
gravity_increase = ConfigurationManager.get("engine", "piece-gravity-increase")
if Controller.key_pressed(pygame.K_DOWN):
self.gravity_time = max(10, (gravity_time - (game.get_level() * gravity_increase)) // 10)
self.set_time = max(10, set_time // 10)
if not Controller.key_pressed(pygame.K_DOWN):
self.gravity_time = gravity_time - (game.get_level() * gravity_increase)
self.set_time = set_time
def draw(self, surface: pygame.Surface, well: Well = None, stack: "Stack" = None) -> None:
tile_size = ConfigurationManager.get("engine", "tile-size")
# ghost piece
if well and stack:
for square in self._get_ghost_piece_points(well, stack):
pygame.draw.polygon(surface, pygame.Color("#FFFFFF"), square, max(tile_size // 6, 1)) # TODO add white to the yaml
super().draw(surface)
# inner border piece
for square in self.points:
if self.inner_border_color:
vertex_one = (square[0][0] + (tile_size // 10), square[0][1] + (tile_size // 10))
vertex_two = (square[1][0] - (tile_size // 10), square[1][1] + (tile_size // 10))
vertex_three = (square[2][0] - (tile_size // 10), square[2][1] - (tile_size // 10))
vertex_four = (square[3][0] + (tile_size // 10), square[3][1] - (tile_size // 10))
new_square = (vertex_one, vertex_two, vertex_three, vertex_four)
pygame.draw.polygon(surface, pygame.Color(self.inner_border_color), new_square, max(tile_size // 6, 1))
def move(self, vector: Tuple) -> None:
self.previous_points = copy.deepcopy(self.points)
self.previous_center = copy.deepcopy(self.center)
self.center[0] += vector[0]
self.center[1] += vector[1]
for square in self.points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
'''
For more information on a rotation of a piece go here:
https://gamedev.stackexchange.com/questions/17974/how-to-rotate-blocks-in-tetris
'''
def rotate(self) -> None:
self.previous_points = copy.deepcopy(self.points)
self.previous_center = copy.deepcopy(self.center)
new_points = []
for square in self.points:
for vertex in square:
h = vertex[0] - self.center[0]
k = vertex[1] - self.center[1]
vertex[0] = (k * -1) + self.center[0]
vertex[1] = h + self.center[1]
new_points.append([square[-1]] + square[0:-1])
self.points = new_points
def revert(self) -> None:
if self.previous_points and self.previous_center:
self.points = self.previous_points
self.center = self.previous_center
def _get_points(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
points = []
for square in shape[:-1]:
sub_points = []
for vertex in square:
point = [vertex[0] * tile_size + position[0], vertex[1] * tile_size + position[1]]
sub_points.append(point)
points.append(sub_points)
return points
def _get_center(self, shape: Tuple, position: Tuple) -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
center = shape[-1]
# cast to int and avoid exception from pygame (center can be a floating point)
return [int(center[0] * tile_size + position[0]), int(center[1] * tile_size + position[1])]
def _apply_gravity(self, elapsed_time: int, well: Well, stack: "Stack") -> bool:
tile_size = ConfigurationManager.get("engine", "tile-size")
self.current_gravity_time += elapsed_time
if self.current_gravity_time >= self.gravity_time:
self.current_gravity_time = 0
if not self._entity_is_below(well) and not self._entity_is_below(stack):
self.move((0, tile_size))
else:
return False
return True
def _apply_set(self, elapsed_time: int, game: "Game") -> bool:
self.current_set_time += elapsed_time
if self.current_set_time >= self.set_time:
self.current_set_time = 0
if self._entity_is_below(game.well) or self._entity_is_below(game.stack):
self._play_piece_set_sound()
game.stack.add_piece(self) # TODO do on tetris object level?
game.current_piece = None # TODO turn into a method
else:
return False
return True
def _mimic_move(self, vector: Tuple) -> List:
mimic_points = copy.deepcopy(self.points)
for square in mimic_points:
for vertex in square:
vertex[0] += vector[0]
vertex[1] += vector[1]
return mimic_points
def _play_piece_set_sound(self) -> None:
piece_set_sound_file = ConfigurationManager.get("sound", "piece-set")
self.piece_set_sound.play(mixer.Sound(piece_set_sound_file))
def _entity_is_below(self, entity: Entity) -> bool:
tile_size = ConfigurationManager.get("engine", "tile-size")
mimic_points = self._mimic_move((0, tile_size))
return entity and entity._collide(mimic_points)
def _get_ghost_piece_points(self, well: Well, stack: "Stack") -> List:
tile_size = ConfigurationManager.get("engine", "tile-size")
prior_points = []
current_points = copy.deepcopy(self.points)
while not well._collide(current_points) and not stack._collide(current_points):
prior_points = copy.deepcopy(current_points)
for square in current_points:
for vertex in square:
vertex[1] += 1 * tile_size
return prior_points
# shape attributes
I_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((3, 0), (4, 0), (4, 1), (3, 1)), (2, 0))
J_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
L_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((2, 0), (3, 0), (3, 1), (2, 1)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1.5, 0.5))
O_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((0, 1), (1, 1), (1, 2), (0, 2)), (1, 1))
S_SHAPE = (((0, 1), (1, 1), (1, 2), (0, 2)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
T_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 0), (3, 0), (3, 1), (2, 1)), (1.5, 0.5))
Z_SHAPE = (((0, 0), (1, 0), (1, 1), (0, 1)), ((1, 0), (2, 0), (2, 1), (1, 1)), ((1, 1), (2, 1), (2, 2), (1, 2)), ((2, 1), (3, 1), (3, 2), (2, 2)), (1.5, 0.5))
"""
TODO description
"""
class Stack(Entity):
def __init__(self, color: str, border_color: str):
super().__init__([], color, border_color)
self.lines_completed_count = 0
self.line_completed_sound = mixer.Channel(1)
def update(self, elapsed_time: int, game: "Game") -> None:
super().update(elapsed_time)
lines_completed = self._complete_rows()
current_level = game.get_level()
points_per_lines_completed = ConfigurationManager.get("engine", "points-per-lines-completed")
game.score += points_per_lines_completed[lines_completed] * (current_level + 1)
self.lines_completed_count += lines_completed
def add_piece(self, piece: Piece) -> None:
self.points += piece.points
# TODO refactor into multiple functions
def _complete_rows(self) -> int:
squares_by_row = {}
for square in self.points:
top_left_vertex = square[0]
if top_left_vertex[1] not in squares_by_row:
squares_by_row[top_left_vertex[1]] = []
if square not in squares_by_row[top_left_vertex[1]]:
squares_by_row[top_left_vertex[1]].append(square)
squares_to_exclude = []
rows_completed = []
for key in squares_by_row:
if len(squares_by_row[key]) == Well.WIDTH:
squares_to_exclude += squares_by_row[key]
rows_completed.append(key)
if len(squares_to_exclude) == 0:
return 0
self._play_line_completed_sound()
tile_size = ConfigurationManager.get("engine", "tile-size")
new_points = []
for square in self.points:
if square not in squares_to_exclude:
for vertex in square:
distance_to_move = 0
for row_completed in rows_completed:
if vertex[1] <= row_completed:
distance_to_move += 1
vertex[1] += tile_size * distance_to_move
new_points.append(square)
self.points = new_points
return len(rows_completed)
def _play_line_completed_sound(self) -> None:
line_completed_sound_file = ConfigurationManager.get("sound", "row-completion")
self.line_completed_sound.play(mixer.Sound(line_completed_sound_file))
"""
TODO description
"""
class PieceGenerator:
_bucket = []
@classmethod
def get_piece(cls, position: Tuple) -> Piece:
if len(cls._bucket) == 0:
cls._generate_bucket()
base_color, inner_border_color, outer_border_color = cls._get_piece_color()
return Piece(cls._get_piece_shape(cls._bucket.pop()), position, base_color, inner_border_color, outer_border_color)
@classmethod
def _generate_bucket(cls) -> None:
piece_types = list(range(7))
while len(cls._bucket) != 7:
random_number = random.randint(0, 6 - len(cls._bucket))
cls._bucket.append(piece_types.pop(random_number))
def _get_piece_shape(piece_number: int) -> Tuple:
if piece_number == 0:
return Piece.I_SHAPE
if piece_number == 1:
return Piece.J_SHAPE
if piece_number == 2:
return Piece.L_SHAPE
if piece_number == 3:
return Piece.O_SHAPE
if piece_number == 4:
return Piece.S_SHAPE
if piece_number == 5:
return Piece.T_SHAPE
if piece_number == 6:
return Piece.Z_SHAPE
return None
def _get_piece_color() -> Tuple:
random_number = random.randint(1, 3)
base_color = ConfigurationManager.get("color", "piece-" + str(random_number))
inner_border_color = None if random_number != 3 else ConfigurationManager.get("color", "piece-inner-border-1")
outer_border_color = ConfigurationManager.get("color", "piece-outer-border-1")
return (base_color, inner_border_color, outer_border_color)

View File

@@ -1,112 +0,0 @@
import sys
import pygame
from pygame import mixer
from tetris.util import ConfigurationManager
from tetris.util import TextGenerator
from tetris.entity import PieceGenerator
from tetris.entity import Well
from tetris.entity import Stack
# TODO should be a singleton and refactor the whole file?
class Game:
def __init__(self):
self.fps = -1
self.tile_size = -1
self.screen = None
self.clock = None
self.main_music = None
self.current_piece = None
self.next_piece = None
self.well = None
self.stack = None
def initialize(self) -> None:
pygame.init()
TextGenerator.load(ConfigurationManager.get("image", "font"), (20, 20))
win_width = ConfigurationManager.get("window", "width")
win_height = ConfigurationManager.get("window", "height")
win_title = ConfigurationManager.get("window", "title")
win_icon = ConfigurationManager.get("image", "window-icon")
self.fps = ConfigurationManager.get("engine", "fps")
self.tile_size = ConfigurationManager.get("engine", "tile-size")
self.screen = pygame.display.set_mode((win_width, win_height))
self.clock = pygame.time.Clock()
self.main_music = mixer.Channel(0)
self.well = Well((280, 80), ConfigurationManager.get("color", "well-1"), ConfigurationManager.get("color", "well-border-1")) # TODO calculate position later and redo color config for well
self.stack = Stack(ConfigurationManager.get("color", "stack-1"), ConfigurationManager.get("color", "stack-border-1"))
self.score = 0
loaded_icon = pygame.image.load(win_icon)
pygame.display.set_caption(win_title)
pygame.display.set_icon(loaded_icon)
main_music_file = ConfigurationManager.get("sound", "main-music")
self.main_music.set_volume(0.7) # TODO add volume to the config
self.main_music.play(mixer.Sound(main_music_file), -1)
# gets called from the games main loop
def update(self) -> None:
# TODO write not initialized exception
elapsed_time = self.clock.tick(self.fps)
if not self.next_piece:
self.next_piece = PieceGenerator.get_piece((620, 160))
if self.current_piece:
self.current_piece.update(elapsed_time, self)
else:
self.current_piece = self.next_piece
self.current_piece.move((360 - 620, 100 - 160)) # TODO calculate spawn position correctly
self.next_piece = PieceGenerator.get_piece((620, 160)) # (360, 100)
if self.stack and self.current_piece.collide(self.stack): # TODO game over redo
pygame.quit()
sys.exit()
if self.stack:
self.stack.update(elapsed_time, self)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
def draw(self) -> None:
# TODO write not initialized exception
# draw window bg
bg_color = pygame.Color(ConfigurationManager.get("color", "window-bg"))
self.screen.fill(bg_color)
# draw all game objects
if self.next_piece:
self.next_piece.draw(self.screen)
if self.well:
self.well.draw(self.screen)
if self.stack:
self.stack.draw(self.screen)
if self.current_piece:
self.current_piece.draw(self.screen, self.well, self.stack)
score = str(self.score).zfill(6)
lines = str(self.stack.lines_completed_count).zfill(4)
level = str(self.get_level()).zfill(2)
TextGenerator.draw("Top", (80, 120), self.screen)
TextGenerator.draw("000000", (80, 140), self.screen)
TextGenerator.draw("Score", (80, 180), self.screen)
TextGenerator.draw(score, (80, 200), self.screen)
TextGenerator.draw("Lines " + lines, (300, 40), self.screen)
TextGenerator.draw("Next", (600, 120), self.screen)
TextGenerator.draw("LVL " + level, (600, 220), self.screen)
# update display
pygame.display.update()
def get_level(self) -> int:
lines_per_level = ConfigurationManager.get("engine", "lines-per-level")
return 0 if not self.stack else self.stack.lines_completed_count // lines_per_level

View File

@@ -1,104 +0,0 @@
import yaml
import pygame
from typing import KeysView, Tuple
"""
TODO description
"""
class ConfigurationManager:
CONFIG_FILE_LOCATION = "config.yaml"
configuration = []
@classmethod
def load(cls) -> None:
with open(cls.CONFIG_FILE_LOCATION, "r") as yaml_file:
cls.configuration = yaml.safe_load(yaml_file)
@classmethod
def get(cls, key: str, sub_key: str = None):
if sub_key:
return cls.configuration[key][sub_key]
else:
return cls.configuration[key]
"""
TODO description
"""
class TextGenerator:
sheet = None
glyph_size = (-1, -1)
characters = { }
@classmethod
def load(cls, file: str, glyph_size: Tuple) -> None:
cls.sheet = pygame.image.load(file)
cls.glyph_size = glyph_size
# load character positions in bitmap into the characters dictionary
# letters
cls.characters["A"] = (9 * glyph_size[0], 2 * glyph_size[1])
cls.characters["B"] = (10 * glyph_size[0], 2 * glyph_size[1])
cls.characters["C"] = (11 * glyph_size[0], 2 * glyph_size[1])
cls.characters["D"] = (0 * glyph_size[0], 3 * glyph_size[1])
cls.characters["E"] = (1 * glyph_size[0], 3 * glyph_size[1])
cls.characters["F"] = (2 * glyph_size[0], 3 * glyph_size[1])
cls.characters["G"] = (3 * glyph_size[0], 3 * glyph_size[1])
cls.characters["H"] = (4 * glyph_size[0], 3 * glyph_size[1])
cls.characters["I"] = (5 * glyph_size[0], 3 * glyph_size[1])
cls.characters["J"] = (6 * glyph_size[0], 3 * glyph_size[1])
cls.characters["K"] = (7 * glyph_size[0], 3 * glyph_size[1])
cls.characters["L"] = (8 * glyph_size[0], 3 * glyph_size[1])
cls.characters["M"] = (9 * glyph_size[0], 3 * glyph_size[1])
cls.characters["N"] = (10 * glyph_size[0], 3 * glyph_size[1])
cls.characters["O"] = (11 * glyph_size[0], 3 * glyph_size[1])
cls.characters["P"] = (0 * glyph_size[0], 4 * glyph_size[1])
cls.characters["Q"] = (1 * glyph_size[0], 4 * glyph_size[1])
cls.characters["R"] = (2 * glyph_size[0], 4 * glyph_size[1])
cls.characters["S"] = (3 * glyph_size[0], 4 * glyph_size[1])
cls.characters["T"] = (4 * glyph_size[0], 4 * glyph_size[1])
cls.characters["U"] = (5 * glyph_size[0], 4 * glyph_size[1])
cls.characters["V"] = (6 * glyph_size[0], 4 * glyph_size[1])
cls.characters["W"] = (7 * glyph_size[0], 4 * glyph_size[1])
cls.characters["X"] = (8 * glyph_size[0], 4 * glyph_size[1])
cls.characters["Y"] = (9 * glyph_size[0], 4 * glyph_size[1])
cls.characters["Z"] = (10 * glyph_size[0], 4 * glyph_size[1])
# numbers
cls.characters["0"] = (4 * glyph_size[0], 1 * glyph_size[1])
cls.characters["1"] = (5 * glyph_size[0], 1 * glyph_size[1])
cls.characters["2"] = (6 * glyph_size[0], 1 * glyph_size[1])
cls.characters["3"] = (7 * glyph_size[0], 1 * glyph_size[1])
cls.characters["4"] = (8 * glyph_size[0], 1 * glyph_size[1])
cls.characters["5"] = (9 * glyph_size[0], 1 * glyph_size[1])
cls.characters["6"] = (10 * glyph_size[0], 1 * glyph_size[1])
cls.characters["7"] = (11 * glyph_size[0], 1 * glyph_size[1])
cls.characters["8"] = (0 * glyph_size[0], 2 * glyph_size[1])
cls.characters["9"] = (1 * glyph_size[0], 2 * glyph_size[1])
@classmethod
def draw(cls, text: str, position: Tuple, surface: pygame.Surface) -> None:
x_position = 0
for char_ in text:
if not char_.isspace():
surface.blit(cls.sheet, (position[0] + x_position, position[1]), pygame.Rect(cls.characters[char_.upper()], (cls.glyph_size[0], cls.glyph_size[1])))
x_position += cls.glyph_size[0]
"""
TODO description
"""
class Controller:
keys_pressed = {}
@classmethod
def key_down(cls, key: int) -> bool:
prior_pressed_state = False if key not in cls.keys_pressed else cls.keys_pressed[key]
cls.keys_pressed[key] = pygame.key.get_pressed()[key]
return cls.keys_pressed[key] and not prior_pressed_state
@classmethod
def key_pressed(cls, key: int) -> bool:
return pygame.key.get_pressed()[key]