Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b69cb4d9ff | ||
|
|
f7b0bc8da4 | ||
|
|
8298aef8b4 | ||
|
|
e9ee2e31fb | ||
|
|
3299732976 | ||
|
|
1631ff8894 | ||
|
|
ac2e568a26 | ||
|
|
714c145316 | ||
|
|
782bf70531 | ||
|
|
c3358a7295 | ||
|
|
f8b18f60fe | ||
|
|
c0e998b882 | ||
|
|
5a78d14c41 | ||
|
|
63fdf72cc8 | ||
|
|
280c6be84d | ||
|
|
a136a6ebf7 | ||
|
|
36edbacbbc | ||
|
|
4eed4a18e9 | ||
|
|
68452de415 | ||
|
|
b8604f41c3 | ||
|
|
ae5b2fc0df | ||
|
|
c35f4798e9 | ||
|
|
79e6fd3a07 | ||
|
|
a8748a69a2 | ||
|
|
38301b0688 | ||
|
|
75d873d1eb | ||
|
|
5200a1458e | ||
|
|
ef3dbad2d2 | ||
|
|
9e47d1bf2d | ||
|
|
a578731b65 | ||
|
|
822dd60bc1 | ||
|
|
3ae98435c2 | ||
|
|
040a879868 |
25
.github/workflows/release.yml
vendored
@ -1,25 +0,0 @@
|
|||||||
name: Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v3
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install build
|
|
||||||
- name: Build package
|
|
||||||
run: python -m build
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
files: dist/*.tar.gz
|
|
||||||
1
.gitignore
vendored
@ -162,3 +162,4 @@ debug
|
|||||||
.logs/
|
.logs/
|
||||||
checkpoints
|
checkpoints
|
||||||
assets/highscore
|
assets/highscore
|
||||||
|
typst/*.pdf
|
||||||
|
|||||||
@ -40,7 +40,7 @@ cd Tetris
|
|||||||
|
|
||||||
3. Install the required dependencies:
|
3. Install the required dependencies:
|
||||||
```bash
|
```bash
|
||||||
pip install .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Run the game:
|
4. Run the game:
|
||||||
@ -54,7 +54,7 @@ python -m tetris
|
|||||||
The `settings.toml` file is a configuration file for customizing various aspects of the game.
|
The `settings.toml` file is a configuration file for customizing various aspects of the game.
|
||||||
|
|
||||||
### General Settings
|
### General Settings
|
||||||
- `pause`: Defines the keys to pause the game. Currently a work in progress (WIP[^WIP]).
|
- `pause`: Defines the keys to pause the game. Currently a work in progress.
|
||||||
- `quit`: Defines the key(s) to quit the game.
|
- `quit`: Defines the key(s) to quit the game.
|
||||||
- `colorscheme`: Specifies the color scheme for the game interface. Options include:
|
- `colorscheme`: Specifies the color scheme for the game interface. Options include:
|
||||||
- `tokyonight-night`
|
- `tokyonight-night`
|
||||||
@ -72,7 +72,7 @@ The `settings.toml` file is a configuration file for customizing various aspects
|
|||||||
- `ccw (counter-clockwise)`: Defines the keys to rotate the tetromino in a counter-clockwise direction.
|
- `ccw (counter-clockwise)`: Defines the keys to rotate the tetromino in a counter-clockwise direction.
|
||||||
|
|
||||||
### Action Settings
|
### Action Settings
|
||||||
- `hold`: Defines the keys to hold the tetromino (WIP).
|
- `hold`: Defines the keys to hold the tetromino (WIP[^WIP]).
|
||||||
- `drop`: Defines the keys to instantly drop the tetromino.
|
- `drop`: Defines the keys to instantly drop the tetromino.
|
||||||
|
|
||||||
### Volume Settings
|
### Volume Settings
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 320 B After Width: | Height: | Size: 4.3 KiB |
@ -4,26 +4,26 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tetris"
|
name = "tetris"
|
||||||
version = "0.1.0"
|
version = "0.1.3"
|
||||||
description = "Tetris game"
|
description = "Tetris game"
|
||||||
authors = [{ name = "Kristofers Solo", email = "dev@kristofers.xyz" }]
|
authors = [{ name = "Kristofers Solo", email = "dev@kristofers.xyz" }]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"attrs==23.1.0",
|
"attrs==23.1.0",
|
||||||
"loguru==0.7.2",
|
"loguru==0.7.2",
|
||||||
"numpy==1.26.3",
|
"numpy==1.26.3",
|
||||||
"pygame-ce==2.4.0",
|
"pygame-ce==2.4.0",
|
||||||
"toml==0.10.2",
|
"toml==0.10.2",
|
||||||
]
|
]
|
||||||
keywords = ["tetris", "game", "pygame"]
|
keywords = ["tetris", "game", "pygame"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"License :: OSI Approved :: GPLv3 License",
|
"License :: OSI Approved :: GPLv3 License",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@ -45,29 +45,29 @@ warn_unused_configs = true
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"B",
|
"B",
|
||||||
"BLE",
|
"BLE",
|
||||||
"C4",
|
"C4",
|
||||||
"ERA",
|
"ERA",
|
||||||
"I",
|
"I",
|
||||||
"ICN",
|
"ICN",
|
||||||
"INP",
|
"INP",
|
||||||
"ISC",
|
"ISC",
|
||||||
"N",
|
"N",
|
||||||
"NPY",
|
"NPY",
|
||||||
"PGH",
|
"PGH",
|
||||||
"PIE",
|
"PIE",
|
||||||
# "PTH",
|
# "PTH",
|
||||||
"Q",
|
"Q",
|
||||||
"RET",
|
"RET",
|
||||||
"RSE",
|
"RSE",
|
||||||
"RUF",
|
"RUF",
|
||||||
"S",
|
"S",
|
||||||
"SIM",
|
"SIM",
|
||||||
"T20",
|
"T20",
|
||||||
"TCH",
|
"TCH",
|
||||||
"TID",
|
"TID",
|
||||||
"YTT",
|
"YTT",
|
||||||
]
|
]
|
||||||
ignore = ["E741"]
|
ignore = ["E741"]
|
||||||
show-fixes = true
|
show-fixes = true
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[General]
|
[General]
|
||||||
pause = ["escape", "F1"] # WIP
|
pause = ["escape", "F1"]
|
||||||
quit = ["q"]
|
quit = ["q"]
|
||||||
colorscheme = "tokyonight-night" # tokyonight-night / tokyonight-storm / tokyonight-day / tokyonight-moon
|
colorscheme = "tokyonight-night" # tokyonight-night / tokyonight-storm / tokyonight-day / tokyonight-moon
|
||||||
|
|
||||||
@ -11,16 +11,16 @@ down = ["down", "keypad 2"]
|
|||||||
[Rotation]
|
[Rotation]
|
||||||
cw = ["x", "up", "keypad 1", "keypad 5", "keypad 9"] # clockwise
|
cw = ["x", "up", "keypad 1", "keypad 5", "keypad 9"] # clockwise
|
||||||
ccw = [
|
ccw = [
|
||||||
"left ctrl",
|
"left ctrl",
|
||||||
"right ctrl",
|
"right ctrl",
|
||||||
"z",
|
"z",
|
||||||
"keypad 3",
|
"keypad 3",
|
||||||
"keypad 7",
|
"keypad 7",
|
||||||
] # counter-clockwise
|
] # counter-clockwise
|
||||||
|
|
||||||
[Action]
|
[Action]
|
||||||
hold = ["left shift", "right shift", "c", "keypad 0"] # WIP
|
hold = ["left shift", "right shift", "c", "keypad 0"] # WIP
|
||||||
drop = ["space", "keypad 5"]
|
drop = ["space", "keypad 8"]
|
||||||
|
|
||||||
[Volume.Music]
|
[Volume.Music]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
from loguru import logger
|
||||||
from utils import CONFIG, Figure, GameMode
|
from utils import CONFIG, Figure, GameMode
|
||||||
|
|
||||||
from .base import BaseScreen
|
from .base import BaseScreen, SceenElement
|
||||||
|
from .pause import Pause
|
||||||
from .preview import Preview
|
from .preview import Preview
|
||||||
from .score import Score
|
from .score import Score
|
||||||
from .tetris import Tetris
|
from .tetris import Tetris
|
||||||
|
|
||||||
|
|
||||||
class Game(BaseScreen):
|
class Game(BaseScreen, SceenElement):
|
||||||
"""
|
"""
|
||||||
Game class.
|
Game class.
|
||||||
|
|
||||||
@ -32,15 +34,16 @@ class Game(BaseScreen):
|
|||||||
def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None:
|
def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None:
|
||||||
self.game_mode = game_mode
|
self.game_mode = game_mode
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
self._initialize_surface()
|
||||||
|
self._initialize_rect()
|
||||||
self._initialize_game_components()
|
self._initialize_game_components()
|
||||||
self._start_background_music()
|
self._start_background_music()
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
"""
|
"""Draw the score on the score surface."""
|
||||||
Raises:
|
self._update_display_surface()
|
||||||
NotImplementedError: Not implemented yet.
|
self._draw_background()
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -51,12 +54,15 @@ class Game(BaseScreen):
|
|||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Run a single iteration of the game loop."""
|
"""Run a single iteration of the game loop."""
|
||||||
|
self.draw()
|
||||||
self.tetris.run()
|
self.tetris.run()
|
||||||
self.score.run()
|
self.score.run()
|
||||||
self.preview.update(self.next_figure)
|
self.preview.update(self.next_figure)
|
||||||
self.preview.run()
|
self.preview.run()
|
||||||
|
|
||||||
|
if self.paused:
|
||||||
|
self.pause_screen.draw()
|
||||||
|
|
||||||
self.clock.tick(CONFIG.game.fps)
|
self.clock.tick(CONFIG.game.fps)
|
||||||
|
|
||||||
def mute(self) -> None:
|
def mute(self) -> None:
|
||||||
@ -64,6 +70,19 @@ class Game(BaseScreen):
|
|||||||
self.music.set_volume(0)
|
self.music.set_volume(0)
|
||||||
self.tetris.mute()
|
self.tetris.mute()
|
||||||
|
|
||||||
|
def pause(self) -> None:
|
||||||
|
"""Pause the game."""
|
||||||
|
if self.paused:
|
||||||
|
logger.debug("Unpause")
|
||||||
|
self.paused = False
|
||||||
|
self.tetris.unfreeze()
|
||||||
|
self.music.play(-1, fade_ms=100)
|
||||||
|
else:
|
||||||
|
logger.debug("Pause")
|
||||||
|
self.paused = True
|
||||||
|
self.tetris.freeze()
|
||||||
|
self.music.fadeout(100)
|
||||||
|
|
||||||
def _initialize_game_components(self) -> None:
|
def _initialize_game_components(self) -> None:
|
||||||
"""Initialize game-related components."""
|
"""Initialize game-related components."""
|
||||||
self.clock = pygame.time.Clock()
|
self.clock = pygame.time.Clock()
|
||||||
@ -72,6 +91,7 @@ class Game(BaseScreen):
|
|||||||
self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings)
|
self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings)
|
||||||
self.score = Score(self.game_mode)
|
self.score = Score(self.game_mode)
|
||||||
self.preview = Preview()
|
self.preview = Preview()
|
||||||
|
self.pause_screen = Pause()
|
||||||
|
|
||||||
def _update_score(self, lines: int, score: int, level: int) -> None:
|
def _update_score(self, lines: int, score: int, level: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -110,3 +130,28 @@ class Game(BaseScreen):
|
|||||||
self.music = pygame.mixer.Sound(CONFIG.music.background)
|
self.music = pygame.mixer.Sound(CONFIG.music.background)
|
||||||
self.music.set_volume(self.settings["Volume"]["Music"]["level"])
|
self.music.set_volume(self.settings["Volume"]["Music"]["level"])
|
||||||
self.music.play(-1)
|
self.music.play(-1)
|
||||||
|
|
||||||
|
def _initialize_surface(self) -> None:
|
||||||
|
"""Initialize the pause screen surface."""
|
||||||
|
self.surface = pygame.Surface(CONFIG.window.size)
|
||||||
|
self.display_surface = pygame.display.get_surface()
|
||||||
|
|
||||||
|
def _initialize_rect(self) -> None:
|
||||||
|
"""Initialize the score rectangle."""
|
||||||
|
self.rect = self.surface.get_rect(topleft=(0, 0))
|
||||||
|
|
||||||
|
def _draw_background(self) -> None:
|
||||||
|
"""Draw the background."""
|
||||||
|
self.surface.fill(CONFIG.colors.bg)
|
||||||
|
|
||||||
|
def _update_display_surface(self) -> None:
|
||||||
|
"""Update the display surface."""
|
||||||
|
self.display_surface.blit(self.surface, self.rect)
|
||||||
|
|
||||||
|
def _draw_border(self) -> None:
|
||||||
|
"""Draw the border.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: Not implemented yet.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from utils import CONFIG, GameMode, read_settings
|
|||||||
from .base import BaseScreen, SceenElement, TextScreen
|
from .base import BaseScreen, SceenElement, TextScreen
|
||||||
from .button import Button
|
from .button import Button
|
||||||
from .game import Game
|
from .game import Game
|
||||||
|
from .tetris import get_keys
|
||||||
|
|
||||||
|
|
||||||
class Main(BaseScreen, SceenElement, TextScreen):
|
class Main(BaseScreen, SceenElement, TextScreen):
|
||||||
@ -45,8 +46,10 @@ class Main(BaseScreen, SceenElement, TextScreen):
|
|||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
self.exit()
|
self.exit()
|
||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if event.key in [pygame.key.key_code(key) for key in self.settings["General"]["quit"]]:
|
if event.key in get_keys(self.settings["General"]["quit"]):
|
||||||
self.exit()
|
self.exit()
|
||||||
|
elif event.key in get_keys(self.settings["General"]["pause"]) and self.game:
|
||||||
|
self.game.pause()
|
||||||
|
|
||||||
if not self.game:
|
if not self.game:
|
||||||
for button in self.buttons:
|
for button in self.buttons:
|
||||||
@ -60,14 +63,13 @@ class Main(BaseScreen, SceenElement, TextScreen):
|
|||||||
|
|
||||||
def run_game_loop(self) -> None:
|
def run_game_loop(self) -> None:
|
||||||
"""Run a single iteration of the game loop."""
|
"""Run a single iteration of the game loop."""
|
||||||
if not self.game:
|
if self.game:
|
||||||
|
self.game.run()
|
||||||
|
else:
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
self.handle_events()
|
self.handle_events()
|
||||||
|
|
||||||
if self.game:
|
|
||||||
self.game.run()
|
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def exit(self) -> None:
|
def exit(self) -> None:
|
||||||
|
|||||||
71
src/game/screens/pause.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import pygame
|
||||||
|
from utils import CONFIG
|
||||||
|
|
||||||
|
from .base import BaseScreen, SceenElement, TextScreen
|
||||||
|
|
||||||
|
|
||||||
|
class Pause(BaseScreen, SceenElement, TextScreen):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._initialize_surface()
|
||||||
|
self._initialize_rect()
|
||||||
|
self._initialize_font()
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: Not implemented yet.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self._update_display_surface()
|
||||||
|
|
||||||
|
def draw(self) -> None:
|
||||||
|
"""Update the display."""
|
||||||
|
self.update()
|
||||||
|
self._draw_background()
|
||||||
|
self._draw_text()
|
||||||
|
|
||||||
|
def _draw_text(self) -> None:
|
||||||
|
"""Draw the text."""
|
||||||
|
self._display_text("Paused")
|
||||||
|
|
||||||
|
def _display_text(self, text: str) -> None:
|
||||||
|
"""Display the text."""
|
||||||
|
text_surface = self.font.render(text, True, CONFIG.colors.fg_float)
|
||||||
|
text_rect = text_surface.get_rect(center=self.rect.center)
|
||||||
|
self.text_surface.blit(text_surface, text_rect)
|
||||||
|
|
||||||
|
def _draw_background(self) -> None:
|
||||||
|
"""Draw the background."""
|
||||||
|
self.surface.fill(CONFIG.colors.bg_float)
|
||||||
|
self.surface.set_alpha(100)
|
||||||
|
self.text_surface.set_colorkey("#000000")
|
||||||
|
self.text_surface.set_alpha(255)
|
||||||
|
|
||||||
|
def _initialize_surface(self) -> None:
|
||||||
|
"""Initialize the pause screen surface."""
|
||||||
|
self.surface = pygame.Surface(CONFIG.window.size)
|
||||||
|
self.display_surface = pygame.display.get_surface()
|
||||||
|
self.text_surface = pygame.Surface(CONFIG.window.size)
|
||||||
|
|
||||||
|
def _initialize_rect(self) -> None:
|
||||||
|
"""Initialize the score rectangle."""
|
||||||
|
self.rect = self.surface.get_rect(topleft=(0, 0))
|
||||||
|
|
||||||
|
def _initialize_font(self) -> None:
|
||||||
|
"""Initialize the font used to display the text."""
|
||||||
|
self.font = pygame.font.Font(CONFIG.font.family, CONFIG.font.size * 2)
|
||||||
|
|
||||||
|
def _update_display_surface(self) -> None:
|
||||||
|
"""Update the display surface."""
|
||||||
|
self.display_surface.blit(self.surface, self.rect)
|
||||||
|
self.display_surface.blit(self.text_surface, self.rect)
|
||||||
|
|
||||||
|
def _draw_border(self) -> None:
|
||||||
|
"""Draw the border.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: Not implemented yet.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
@ -30,7 +30,7 @@ class Preview(BaseScreen, SceenElement):
|
|||||||
Args:
|
Args:
|
||||||
next_figures: Next figure.
|
next_figures: Next figure.
|
||||||
"""
|
"""
|
||||||
self.next_figure = next_figure
|
self.next_figure: Figure = next_figure
|
||||||
|
|
||||||
def draw(self) -> None:
|
def draw(self) -> None:
|
||||||
"""Draw the preview on the preview surface."""
|
"""Draw the preview on the preview surface."""
|
||||||
@ -51,9 +51,10 @@ class Preview(BaseScreen, SceenElement):
|
|||||||
|
|
||||||
def _draw_figure(self) -> None:
|
def _draw_figure(self) -> None:
|
||||||
"""Draw a single upcoming figure on the preview surface."""
|
"""Draw a single upcoming figure on the preview surface."""
|
||||||
figure_surface = self.next_figure.value.image
|
figure_surface: pygame.Surface = self.next_figure.value.image()
|
||||||
x = self.surface.get_width() / 2
|
x = self.surface.get_width() / 2
|
||||||
y = self.surface.get_height() / 2
|
y = self.surface.get_height() / 2
|
||||||
|
figure_surface.fill(self.next_figure.value.color, special_flags=pygame.BLEND_RGB_MULT)
|
||||||
rect = figure_surface.get_rect(center=(x, y))
|
rect = figure_surface.get_rect(center=(x, y))
|
||||||
self.surface.blit(figure_surface, rect)
|
self.surface.blit(figure_surface, rect)
|
||||||
|
|
||||||
|
|||||||
@ -91,12 +91,11 @@ class Tetris(BaseScreen):
|
|||||||
|
|
||||||
def handle_events(self) -> None:
|
def handle_events(self) -> None:
|
||||||
"""Handle player input events."""
|
"""Handle player input events."""
|
||||||
keys: pygame.key.ScancodeWrapper = pygame.key.get_pressed()
|
if not self.paused:
|
||||||
|
self._handle_movement_keys()
|
||||||
self._handle_movement_keys(keys)
|
self._handle_rotation_keys()
|
||||||
self._handle_rotation_keys(keys)
|
self._handle_down_key()
|
||||||
self._handle_down_key(keys)
|
self._handle_drop_key()
|
||||||
self._handle_drop_key(keys)
|
|
||||||
|
|
||||||
def move_down(self) -> bool:
|
def move_down(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -161,6 +160,25 @@ class Tetris(BaseScreen):
|
|||||||
"""
|
"""
|
||||||
return self.tetromino.drop()
|
return self.tetromino.drop()
|
||||||
|
|
||||||
|
def restart(self) -> None:
|
||||||
|
"""Restart the game."""
|
||||||
|
logger.info(f"Restarting the game. Score was {self.score}")
|
||||||
|
self._reset_game_state()
|
||||||
|
|
||||||
|
def freeze(self) -> None:
|
||||||
|
"""Freeze all timers."""
|
||||||
|
self.timers.freeze()
|
||||||
|
self.paused = True
|
||||||
|
|
||||||
|
def unfreeze(self) -> None:
|
||||||
|
"""Unfreeze all timers."""
|
||||||
|
self.timers.unfreeze()
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
def mute(self) -> None:
|
||||||
|
"""Mute the game."""
|
||||||
|
self.landing_sound.set_volume(0)
|
||||||
|
|
||||||
def create_new_tetromino(self, shape: Optional[Figure] = None) -> Optional[Tetromino]:
|
def create_new_tetromino(self, shape: Optional[Figure] = None) -> Optional[Tetromino]:
|
||||||
"""Create a new tetromino and perform necessary actions."""
|
"""Create a new tetromino and perform necessary actions."""
|
||||||
self._play_landing_sound()
|
self._play_landing_sound()
|
||||||
@ -204,15 +222,6 @@ class Tetris(BaseScreen):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restart(self) -> None:
|
|
||||||
"""Restart the game."""
|
|
||||||
logger.info(f"Restarting the game. Score was {self.score}")
|
|
||||||
self._reset_game_state()
|
|
||||||
|
|
||||||
def mute(self) -> None:
|
|
||||||
"""Mute the game."""
|
|
||||||
self.landing_sound.set_volume(0)
|
|
||||||
|
|
||||||
def _draw_grid(self) -> None:
|
def _draw_grid(self) -> None:
|
||||||
"""Draw the grid on the game surface."""
|
"""Draw the grid on the game surface."""
|
||||||
for col in range(1, CONFIG.game.columns):
|
for col in range(1, CONFIG.game.columns):
|
||||||
@ -299,8 +308,8 @@ class Tetris(BaseScreen):
|
|||||||
def _level_up(self) -> None:
|
def _level_up(self) -> None:
|
||||||
"""Level up."""
|
"""Level up."""
|
||||||
self.level += 1
|
self.level += 1
|
||||||
self.initial_block_speed *= 0.5
|
self.initial_block_speed *= 0.8 # the larger the multiplier, the slower the game
|
||||||
self.increased_block_speed *= 0.5
|
self.increased_block_speed *= 0.8
|
||||||
self.timers.vertical.duration = self.initial_block_speed
|
self.timers.vertical.duration = self.initial_block_speed
|
||||||
|
|
||||||
def _draw_components(self) -> None:
|
def _draw_components(self) -> None:
|
||||||
@ -365,13 +374,14 @@ class Tetris(BaseScreen):
|
|||||||
def _initialize_game_state(self) -> None:
|
def _initialize_game_state(self) -> None:
|
||||||
"""Initialize the game state."""
|
"""Initialize the game state."""
|
||||||
self.initial_block_speed = CONFIG.game.initial_speed
|
self.initial_block_speed = CONFIG.game.initial_speed
|
||||||
self.increased_block_speed = self.initial_block_speed * 0.4
|
self.increased_block_speed = self.initial_block_speed * 0.5
|
||||||
self.down_pressed = False
|
self.down_pressed = False
|
||||||
self.drop_pressed = False
|
self.drop_pressed = False
|
||||||
self.level: int = 1
|
self.level: int = 1
|
||||||
self.score: int = 0
|
self.score: int = 0
|
||||||
self.lines: int = 0
|
self.lines: int = 0
|
||||||
self.game_over = False
|
self.game_over = False
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
def _initialize_sound(self) -> None:
|
def _initialize_sound(self) -> None:
|
||||||
"""Initialize game sounds."""
|
"""Initialize game sounds."""
|
||||||
@ -397,17 +407,17 @@ class Tetris(BaseScreen):
|
|||||||
"""Fill the game surface with background color."""
|
"""Fill the game surface with background color."""
|
||||||
self.surface.fill(CONFIG.colors.bg_float)
|
self.surface.fill(CONFIG.colors.bg_float)
|
||||||
|
|
||||||
def _handle_movement_keys(self, keys: pygame.key.ScancodeWrapper) -> None:
|
def _handle_movement_keys(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handle movement keys.
|
Handle movement keys.
|
||||||
|
|
||||||
See `settings.toml` for the default key bindings.
|
See `settings.toml` for the default key bindings.
|
||||||
"""
|
"""
|
||||||
right_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["right"]]
|
right_keys = get_keys(self.settings["Movement"]["right"])
|
||||||
right_key_pressed = any(keys[key] for key in right_keys)
|
right_key_pressed = is_key_pressed(right_keys)
|
||||||
|
|
||||||
left_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["left"]]
|
left_keys = get_keys(self.settings["Movement"]["left"])
|
||||||
left_key_pressed = any(keys[key] for key in left_keys)
|
left_key_pressed = is_key_pressed(left_keys)
|
||||||
|
|
||||||
if not self.timers.horizontal.active:
|
if not self.timers.horizontal.active:
|
||||||
if left_key_pressed:
|
if left_key_pressed:
|
||||||
@ -417,17 +427,18 @@ class Tetris(BaseScreen):
|
|||||||
self.move_right()
|
self.move_right()
|
||||||
self.timers.horizontal.activate()
|
self.timers.horizontal.activate()
|
||||||
|
|
||||||
def _handle_rotation_keys(self, keys: pygame.key.ScancodeWrapper) -> None:
|
def _handle_rotation_keys(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handle rotation keys.
|
Handle rotation keys.
|
||||||
|
|
||||||
See `settings.toml` for the default key bindings.
|
See `settings.toml` for the default key bindings.
|
||||||
"""
|
"""
|
||||||
cw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["cw"]]
|
|
||||||
cw_key_pressed = any(keys[key] for key in cw_keys)
|
|
||||||
|
|
||||||
ccw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["ccw"]]
|
cw_keys = get_keys(self.settings["Rotation"]["cw"])
|
||||||
ccw_key_pressed = any(keys[key] for key in ccw_keys)
|
cw_key_pressed = is_key_pressed(cw_keys)
|
||||||
|
|
||||||
|
ccw_keys = get_keys(self.settings["Rotation"]["ccw"])
|
||||||
|
ccw_key_pressed = is_key_pressed(ccw_keys)
|
||||||
|
|
||||||
if not self.timers.rotation.active:
|
if not self.timers.rotation.active:
|
||||||
if cw_key_pressed:
|
if cw_key_pressed:
|
||||||
@ -438,14 +449,14 @@ class Tetris(BaseScreen):
|
|||||||
self.rotate_reverse()
|
self.rotate_reverse()
|
||||||
self.timers.rotation.activate()
|
self.timers.rotation.activate()
|
||||||
|
|
||||||
def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> None:
|
def _handle_down_key(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handle the down key.
|
Handle the down key.
|
||||||
|
|
||||||
See `settings.toml` for the default key bindings.
|
See `settings.toml` for the default key bindings.
|
||||||
"""
|
"""
|
||||||
down_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["down"]]
|
down_keys = get_keys(self.settings["Movement"]["down"])
|
||||||
down_key_pressed = any(keys[key] for key in down_keys)
|
down_key_pressed = is_key_pressed(down_keys)
|
||||||
if not self.down_pressed and down_key_pressed:
|
if not self.down_pressed and down_key_pressed:
|
||||||
self.down_pressed = True
|
self.down_pressed = True
|
||||||
self.timers.vertical.duration = self.increased_block_speed
|
self.timers.vertical.duration = self.increased_block_speed
|
||||||
@ -454,14 +465,14 @@ class Tetris(BaseScreen):
|
|||||||
self.down_pressed = False
|
self.down_pressed = False
|
||||||
self.timers.vertical.duration = self.initial_block_speed
|
self.timers.vertical.duration = self.initial_block_speed
|
||||||
|
|
||||||
def _handle_drop_key(self, keys: pygame.key.ScancodeWrapper) -> None:
|
def _handle_drop_key(self) -> None:
|
||||||
"""
|
"""
|
||||||
Handle the drop key.
|
Handle the drop key.
|
||||||
|
|
||||||
See `settings.toml` for the default key bindings.
|
See `settings.toml` for the default key bindings.
|
||||||
"""
|
"""
|
||||||
drop_keys = [pygame.key.key_code(key) for key in self.settings["Action"]["drop"]]
|
drop_keys = get_keys(self.settings["Action"]["drop"])
|
||||||
drop_key_pressed = any(keys[key] for key in drop_keys)
|
drop_key_pressed = is_key_pressed(drop_keys)
|
||||||
|
|
||||||
if not self.timers.drop.active and drop_key_pressed:
|
if not self.timers.drop.active and drop_key_pressed:
|
||||||
self.drop()
|
self.drop()
|
||||||
@ -494,3 +505,14 @@ class Tetris(BaseScreen):
|
|||||||
(self.grid_surface.get_width(), y),
|
(self.grid_surface.get_width(), y),
|
||||||
CONFIG.game.line_width,
|
CONFIG.game.line_width,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_keys(keys: dict[str, str]) -> list[int]:
|
||||||
|
"""Get the key codes for the specified keys."""
|
||||||
|
return [pygame.key.key_code(key) for key in keys]
|
||||||
|
|
||||||
|
|
||||||
|
def is_key_pressed(keys: list[int]) -> bool:
|
||||||
|
"""Check if any of the specified keys is pressed."""
|
||||||
|
keys_pressed = pygame.key.get_pressed()
|
||||||
|
return any(keys_pressed[key] for key in keys)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Callable, NamedTuple, Optional
|
from typing import Any, Callable, Iterator, Optional
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
from attrs import define, field
|
from attrs import define, field
|
||||||
@ -55,7 +55,8 @@ class Timer:
|
|||||||
self.activate()
|
self.activate()
|
||||||
|
|
||||||
|
|
||||||
class Timers(NamedTuple):
|
@define
|
||||||
|
class Timers:
|
||||||
"""
|
"""
|
||||||
NamedTuple for grouping different timers.
|
NamedTuple for grouping different timers.
|
||||||
|
|
||||||
@ -70,3 +71,17 @@ class Timers(NamedTuple):
|
|||||||
horizontal: Timer
|
horizontal: Timer
|
||||||
rotation: Timer
|
rotation: Timer
|
||||||
drop: Timer
|
drop: Timer
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Timer]:
|
||||||
|
"""Returns an iterator over the timers."""
|
||||||
|
return iter((self.vertical, self.horizontal, self.rotation, self.drop))
|
||||||
|
|
||||||
|
def freeze(self) -> None:
|
||||||
|
"""Freezes all timers."""
|
||||||
|
for timer in self:
|
||||||
|
timer.deactivate()
|
||||||
|
|
||||||
|
def unfreeze(self) -> None:
|
||||||
|
"""Unfreezes all timers."""
|
||||||
|
for timer in self:
|
||||||
|
timer.activate()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from .config import CONFIG
|
from .config import CONFIG
|
||||||
from .enum import Direction, GameMode, Rotation
|
from .enum import Direction, GameMode, Rotation
|
||||||
from .figure import Figure, FigureConfig
|
from .figure import Figure
|
||||||
from .path import BASE_PATH
|
from .path import BASE_PATH
|
||||||
from .settings import read_settings, save_settings
|
from .settings import read_settings, save_settings
|
||||||
from .tuples import Size
|
from .tuples import Size
|
||||||
@ -10,7 +10,6 @@ __all__ = [
|
|||||||
"CONFIG",
|
"CONFIG",
|
||||||
"Size",
|
"Size",
|
||||||
"Figure",
|
"Figure",
|
||||||
"FigureConfig",
|
|
||||||
"Direction",
|
"Direction",
|
||||||
"Rotation",
|
"Rotation",
|
||||||
"GameMode",
|
"GameMode",
|
||||||
|
|||||||
@ -1,30 +1,33 @@
|
|||||||
import random
|
import random
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
from attrs import define
|
||||||
from pygame import Vector2 as Vec2
|
from pygame import Vector2 as Vec2
|
||||||
|
|
||||||
from .colors import TokyoNightNight
|
from .config import CONFIG
|
||||||
from .path import BASE_PATH
|
from .path import BASE_PATH
|
||||||
|
|
||||||
|
|
||||||
class FigureConfig(NamedTuple):
|
@define
|
||||||
|
class FigureParams:
|
||||||
"""
|
"""
|
||||||
Attributes:
|
Attributes:
|
||||||
shape: The shape of the figure.
|
shape: The shape of the figure.
|
||||||
color: The color of the figure.
|
color: The color of the figure.
|
||||||
image: The image of the figure.
|
filename: The filename of the image of the figure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
shape: list[Vec2]
|
shape: list[Vec2]
|
||||||
color: str
|
color: str
|
||||||
image: pygame.Surface
|
filename: str
|
||||||
|
|
||||||
|
def image(self) -> pygame.Surface:
|
||||||
def _load_image(filename: str) -> pygame.Surface:
|
"""
|
||||||
return pygame.image.load(BASE_PATH / "assets" / "figures" / filename) # TODO: add `.convert_alpha()``
|
Returns:
|
||||||
# TODO: change colors of images
|
The image of the figure.
|
||||||
|
"""
|
||||||
|
return pygame.image.load(BASE_PATH / "assets" / "figures" / self.filename).convert_alpha()
|
||||||
|
|
||||||
|
|
||||||
class Figure(Enum):
|
class Figure(Enum):
|
||||||
@ -39,76 +42,76 @@ class Figure(Enum):
|
|||||||
L: The L figure.
|
L: The L figure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
I = FigureConfig(
|
I = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(0, -2),
|
Vec2(0, -2),
|
||||||
Vec2(0, 1),
|
Vec2(0, 1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().cyan,
|
CONFIG.colors.cyan,
|
||||||
_load_image("I.png"),
|
"I.png",
|
||||||
)
|
)
|
||||||
O = FigureConfig(
|
O = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(1, 0),
|
Vec2(1, 0),
|
||||||
Vec2(1, -1),
|
Vec2(1, -1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().yellow,
|
CONFIG.colors.yellow,
|
||||||
_load_image("O.png"),
|
"O.png",
|
||||||
)
|
)
|
||||||
T = FigureConfig(
|
T = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(-1, 0),
|
Vec2(-1, 0),
|
||||||
Vec2(1, 0),
|
Vec2(1, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().purple,
|
CONFIG.colors.purple,
|
||||||
_load_image("T.png"),
|
"T.png",
|
||||||
)
|
)
|
||||||
|
|
||||||
S = FigureConfig(
|
S = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(-1, 0),
|
Vec2(-1, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(1, -1),
|
Vec2(1, -1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().green,
|
CONFIG.colors.green,
|
||||||
_load_image("S.png"),
|
"S.png",
|
||||||
)
|
)
|
||||||
Z = FigureConfig(
|
Z = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(1, 0),
|
Vec2(1, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(-1, -1),
|
Vec2(-1, -1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().red,
|
CONFIG.colors.red,
|
||||||
_load_image("Z.png"),
|
"Z.png",
|
||||||
)
|
)
|
||||||
J = FigureConfig(
|
J = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(0, 1),
|
Vec2(0, 1),
|
||||||
Vec2(-1, 1),
|
Vec2(-1, 1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().blue,
|
CONFIG.colors.blue,
|
||||||
_load_image("J.png"),
|
"J.png",
|
||||||
)
|
)
|
||||||
L = FigureConfig(
|
L = FigureParams( # type: ignore
|
||||||
[
|
[
|
||||||
Vec2(0, 0),
|
Vec2(0, 0),
|
||||||
Vec2(0, -1),
|
Vec2(0, -1),
|
||||||
Vec2(0, 1),
|
Vec2(0, 1),
|
||||||
Vec2(1, 1),
|
Vec2(1, 1),
|
||||||
],
|
],
|
||||||
TokyoNightNight().orange,
|
CONFIG.colors.orange,
|
||||||
_load_image("L.png"),
|
"L.png",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
BIN
typst/main.pdf
@ -99,7 +99,7 @@ cd Tetris
|
|||||||
|
|
||||||
3. Instalējiet nepieciešamās atkarības:
|
3. Instalējiet nepieciešamās atkarības:
|
||||||
```bash
|
```bash
|
||||||
pip install .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Palaidiet spēli:
|
4. Palaidiet spēli:
|
||||||
@ -116,7 +116,7 @@ python -m tetris
|
|||||||
|
|
||||||
=== Vispārīgi iestatījumi
|
=== Vispārīgi iestatījumi
|
||||||
#par(first-line-indent: 0cm, [
|
#par(first-line-indent: 0cm, [
|
||||||
/ `pause`: definē taustiņu(-s), lai apturētu spēli (WIP #footnote[WIP (Work In Progress) -- nepabeigts darbs: darbs vai produkts, kas ir sākts, bet nav pabeigts vai gatavs.]<WIP>).
|
/ `pause`: definē taustiņu(-s), lai apturētu spēli.
|
||||||
/ `quit`: definē taustiņu(-s), lai izietu no spēles.
|
/ `quit`: definē taustiņu(-s), lai izietu no spēles.
|
||||||
/ `colorscheme`: norāda spēles saskarnes krāsu shēmu. Iespējas ietver:
|
/ `colorscheme`: norāda spēles saskarnes krāsu shēmu. Iespējas ietver:
|
||||||
- `tokyonight-day`
|
- `tokyonight-day`
|
||||||
@ -140,7 +140,7 @@ python -m tetris
|
|||||||
|
|
||||||
=== Papildus darbību iestatījumi
|
=== Papildus darbību iestatījumi
|
||||||
#par(first-line-indent: 0cm, [
|
#par(first-line-indent: 0cm, [
|
||||||
/ `hold`: definē taustiņu(-s), lai uzglabātu tetromino (WIP @WIP).
|
/ `hold`: definē taustiņu(-s), lai uzglabātu tetromino (WIP #footnote[WIP (Work In Progress) -- nepabeigts darbs: darbs vai produkts, kas ir sākts, bet nav pabeigts vai gatavs.]<WIP>).
|
||||||
/ `drop`: definē taustiņ(-s), lai nekavējoties nomestu tetromino.
|
/ `drop`: definē taustiņ(-s), lai nekavējoties nomestu tetromino.
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||