Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

22 changed files with 155 additions and 289 deletions

25
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,25 @@
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
View File

@ -162,4 +162,3 @@ debug
.logs/
checkpoints
assets/highscore
typst/*.pdf

View File

@ -40,7 +40,7 @@ cd Tetris
3. Install the required dependencies:
```bash
pip install -e .
pip install .
```
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.
### General Settings
- `pause`: Defines the keys to pause the game. Currently a work in progress.
- `pause`: Defines the keys to pause the game. Currently a work in progress (WIP[^WIP]).
- `quit`: Defines the key(s) to quit the game.
- `colorscheme`: Specifies the color scheme for the game interface. Options include:
- `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.
### Action Settings
- `hold`: Defines the keys to hold the tetromino (WIP[^WIP]).
- `hold`: Defines the keys to hold the tetromino (WIP).
- `drop`: Defines the keys to instantly drop the tetromino.
### Volume Settings

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 320 B

View File

@ -4,26 +4,26 @@ build-backend = "setuptools.build_meta"
[project]
name = "tetris"
version = "0.1.3"
version = "0.1.0"
description = "Tetris game"
authors = [{ name = "Kristofers Solo", email = "dev@kristofers.xyz" }]
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
dependencies = [
"attrs==23.1.0",
"loguru==0.7.2",
"numpy==1.26.3",
"pygame-ce==2.4.0",
"toml==0.10.2",
"attrs==23.1.0",
"loguru==0.7.2",
"numpy==1.26.3",
"pygame-ce==2.4.0",
"toml==0.10.2",
]
keywords = ["tetris", "game", "pygame"]
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: GPLv3 License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: GPLv3 License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
[project.urls]
@ -45,29 +45,29 @@ warn_unused_configs = true
[tool.ruff]
extend-select = [
"B",
"BLE",
"C4",
"ERA",
"I",
"ICN",
"INP",
"ISC",
"N",
"NPY",
"PGH",
"PIE",
# "PTH",
"Q",
"RET",
"RSE",
"RUF",
"S",
"SIM",
"T20",
"TCH",
"TID",
"YTT",
"B",
"BLE",
"C4",
"ERA",
"I",
"ICN",
"INP",
"ISC",
"N",
"NPY",
"PGH",
"PIE",
# "PTH",
"Q",
"RET",
"RSE",
"RUF",
"S",
"SIM",
"T20",
"TCH",
"TID",
"YTT",
]
ignore = ["E741"]
show-fixes = true

View File

@ -1,5 +1,5 @@
[General]
pause = ["escape", "F1"]
pause = ["escape", "F1"] # WIP
quit = ["q"]
colorscheme = "tokyonight-night" # tokyonight-night / tokyonight-storm / tokyonight-day / tokyonight-moon
@ -11,16 +11,16 @@ down = ["down", "keypad 2"]
[Rotation]
cw = ["x", "up", "keypad 1", "keypad 5", "keypad 9"] # clockwise
ccw = [
"left ctrl",
"right ctrl",
"z",
"keypad 3",
"keypad 7",
"left ctrl",
"right ctrl",
"z",
"keypad 3",
"keypad 7",
] # counter-clockwise
[Action]
hold = ["left shift", "right shift", "c", "keypad 0"] # WIP
drop = ["space", "keypad 8"]
drop = ["space", "keypad 5"]
[Volume.Music]
enabled = true

View File

@ -1,17 +1,15 @@
from typing import Any
import pygame
from loguru import logger
from utils import CONFIG, Figure, GameMode
from .base import BaseScreen, SceenElement
from .pause import Pause
from .base import BaseScreen
from .preview import Preview
from .score import Score
from .tetris import Tetris
class Game(BaseScreen, SceenElement):
class Game(BaseScreen):
"""
Game class.
@ -34,16 +32,15 @@ class Game(BaseScreen, SceenElement):
def __init__(self, game_mode: GameMode, settings: dict[str, Any]) -> None:
self.game_mode = game_mode
self.settings = settings
self._initialize_surface()
self._initialize_rect()
self._initialize_game_components()
self._start_background_music()
self.paused = False
def draw(self) -> None:
"""Draw the score on the score surface."""
self._update_display_surface()
self._draw_background()
"""
Raises:
NotImplementedError: Not implemented yet.
"""
raise NotImplementedError
def update(self) -> None:
"""
@ -54,15 +51,12 @@ class Game(BaseScreen, SceenElement):
def run(self) -> None:
"""Run a single iteration of the game loop."""
self.draw()
self.tetris.run()
self.score.run()
self.preview.update(self.next_figure)
self.preview.run()
if self.paused:
self.pause_screen.draw()
self.clock.tick(CONFIG.game.fps)
def mute(self) -> None:
@ -70,19 +64,6 @@ class Game(BaseScreen, SceenElement):
self.music.set_volume(0)
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:
"""Initialize game-related components."""
self.clock = pygame.time.Clock()
@ -91,7 +72,6 @@ class Game(BaseScreen, SceenElement):
self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings)
self.score = Score(self.game_mode)
self.preview = Preview()
self.pause_screen = Pause()
def _update_score(self, lines: int, score: int, level: int) -> None:
"""
@ -130,28 +110,3 @@ class Game(BaseScreen, SceenElement):
self.music = pygame.mixer.Sound(CONFIG.music.background)
self.music.set_volume(self.settings["Volume"]["Music"]["level"])
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

View File

@ -8,7 +8,6 @@ from utils import CONFIG, GameMode, read_settings
from .base import BaseScreen, SceenElement, TextScreen
from .button import Button
from .game import Game
from .tetris import get_keys
class Main(BaseScreen, SceenElement, TextScreen):
@ -46,10 +45,8 @@ class Main(BaseScreen, SceenElement, TextScreen):
if event.type == pygame.QUIT:
self.exit()
elif event.type == pygame.KEYDOWN:
if event.key in get_keys(self.settings["General"]["quit"]):
if event.key in [pygame.key.key_code(key) for key in self.settings["General"]["quit"]]:
self.exit()
elif event.key in get_keys(self.settings["General"]["pause"]) and self.game:
self.game.pause()
if not self.game:
for button in self.buttons:
@ -63,13 +60,14 @@ class Main(BaseScreen, SceenElement, TextScreen):
def run_game_loop(self) -> None:
"""Run a single iteration of the game loop."""
if self.game:
self.game.run()
else:
if not self.game:
self.draw()
self.handle_events()
if self.game:
self.game.run()
self.update()
def exit(self) -> None:

View File

@ -1,71 +0,0 @@
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

View File

@ -30,7 +30,7 @@ class Preview(BaseScreen, SceenElement):
Args:
next_figures: Next figure.
"""
self.next_figure: Figure = next_figure
self.next_figure = next_figure
def draw(self) -> None:
"""Draw the preview on the preview surface."""
@ -51,10 +51,9 @@ class Preview(BaseScreen, SceenElement):
def _draw_figure(self) -> None:
"""Draw a single upcoming figure on the preview surface."""
figure_surface: pygame.Surface = self.next_figure.value.image()
figure_surface = self.next_figure.value.image
x = self.surface.get_width() / 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))
self.surface.blit(figure_surface, rect)

View File

@ -91,11 +91,12 @@ class Tetris(BaseScreen):
def handle_events(self) -> None:
"""Handle player input events."""
if not self.paused:
self._handle_movement_keys()
self._handle_rotation_keys()
self._handle_down_key()
self._handle_drop_key()
keys: pygame.key.ScancodeWrapper = pygame.key.get_pressed()
self._handle_movement_keys(keys)
self._handle_rotation_keys(keys)
self._handle_down_key(keys)
self._handle_drop_key(keys)
def move_down(self) -> bool:
"""
@ -160,25 +161,6 @@ class Tetris(BaseScreen):
"""
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]:
"""Create a new tetromino and perform necessary actions."""
self._play_landing_sound()
@ -222,6 +204,15 @@ class Tetris(BaseScreen):
return True
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:
"""Draw the grid on the game surface."""
for col in range(1, CONFIG.game.columns):
@ -308,8 +299,8 @@ class Tetris(BaseScreen):
def _level_up(self) -> None:
"""Level up."""
self.level += 1
self.initial_block_speed *= 0.8 # the larger the multiplier, the slower the game
self.increased_block_speed *= 0.8
self.initial_block_speed *= 0.5
self.increased_block_speed *= 0.5
self.timers.vertical.duration = self.initial_block_speed
def _draw_components(self) -> None:
@ -374,14 +365,13 @@ class Tetris(BaseScreen):
def _initialize_game_state(self) -> None:
"""Initialize the game state."""
self.initial_block_speed = CONFIG.game.initial_speed
self.increased_block_speed = self.initial_block_speed * 0.5
self.increased_block_speed = self.initial_block_speed * 0.4
self.down_pressed = False
self.drop_pressed = False
self.level: int = 1
self.score: int = 0
self.lines: int = 0
self.game_over = False
self.paused = False
def _initialize_sound(self) -> None:
"""Initialize game sounds."""
@ -407,17 +397,17 @@ class Tetris(BaseScreen):
"""Fill the game surface with background color."""
self.surface.fill(CONFIG.colors.bg_float)
def _handle_movement_keys(self) -> None:
def _handle_movement_keys(self, keys: pygame.key.ScancodeWrapper) -> None:
"""
Handle movement keys.
See `settings.toml` for the default key bindings.
"""
right_keys = get_keys(self.settings["Movement"]["right"])
right_key_pressed = is_key_pressed(right_keys)
right_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["right"]]
right_key_pressed = any(keys[key] for key in right_keys)
left_keys = get_keys(self.settings["Movement"]["left"])
left_key_pressed = is_key_pressed(left_keys)
left_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["left"]]
left_key_pressed = any(keys[key] for key in left_keys)
if not self.timers.horizontal.active:
if left_key_pressed:
@ -427,18 +417,17 @@ class Tetris(BaseScreen):
self.move_right()
self.timers.horizontal.activate()
def _handle_rotation_keys(self) -> None:
def _handle_rotation_keys(self, keys: pygame.key.ScancodeWrapper) -> None:
"""
Handle rotation keys.
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)
cw_keys = get_keys(self.settings["Rotation"]["cw"])
cw_key_pressed = is_key_pressed(cw_keys)
ccw_keys = get_keys(self.settings["Rotation"]["ccw"])
ccw_key_pressed = is_key_pressed(ccw_keys)
ccw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["ccw"]]
ccw_key_pressed = any(keys[key] for key in ccw_keys)
if not self.timers.rotation.active:
if cw_key_pressed:
@ -449,14 +438,14 @@ class Tetris(BaseScreen):
self.rotate_reverse()
self.timers.rotation.activate()
def _handle_down_key(self) -> None:
def _handle_down_key(self, keys: pygame.key.ScancodeWrapper) -> None:
"""
Handle the down key.
See `settings.toml` for the default key bindings.
"""
down_keys = get_keys(self.settings["Movement"]["down"])
down_key_pressed = is_key_pressed(down_keys)
down_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["down"]]
down_key_pressed = any(keys[key] for key in down_keys)
if not self.down_pressed and down_key_pressed:
self.down_pressed = True
self.timers.vertical.duration = self.increased_block_speed
@ -465,14 +454,14 @@ class Tetris(BaseScreen):
self.down_pressed = False
self.timers.vertical.duration = self.initial_block_speed
def _handle_drop_key(self) -> None:
def _handle_drop_key(self, keys: pygame.key.ScancodeWrapper) -> None:
"""
Handle the drop key.
See `settings.toml` for the default key bindings.
"""
drop_keys = get_keys(self.settings["Action"]["drop"])
drop_key_pressed = is_key_pressed(drop_keys)
drop_keys = [pygame.key.key_code(key) for key in self.settings["Action"]["drop"]]
drop_key_pressed = any(keys[key] for key in drop_keys)
if not self.timers.drop.active and drop_key_pressed:
self.drop()
@ -505,14 +494,3 @@ class Tetris(BaseScreen):
(self.grid_surface.get_width(), y),
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)

View File

@ -1,4 +1,4 @@
from typing import Any, Callable, Iterator, Optional
from typing import Any, Callable, NamedTuple, Optional
import pygame
from attrs import define, field
@ -55,8 +55,7 @@ class Timer:
self.activate()
@define
class Timers:
class Timers(NamedTuple):
"""
NamedTuple for grouping different timers.
@ -71,17 +70,3 @@ class Timers:
horizontal: Timer
rotation: 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()

View File

@ -1,6 +1,6 @@
from .config import CONFIG
from .enum import Direction, GameMode, Rotation
from .figure import Figure
from .figure import Figure, FigureConfig
from .path import BASE_PATH
from .settings import read_settings, save_settings
from .tuples import Size
@ -10,6 +10,7 @@ __all__ = [
"CONFIG",
"Size",
"Figure",
"FigureConfig",
"Direction",
"Rotation",
"GameMode",

View File

@ -1,33 +1,30 @@
import random
from enum import Enum
from typing import NamedTuple
import pygame
from attrs import define
from pygame import Vector2 as Vec2
from .config import CONFIG
from .colors import TokyoNightNight
from .path import BASE_PATH
@define
class FigureParams:
class FigureConfig(NamedTuple):
"""
Attributes:
shape: The shape of the figure.
color: The color of the figure.
filename: The filename of the image of the figure.
image: The image of the figure.
"""
shape: list[Vec2]
color: str
filename: str
image: pygame.Surface
def image(self) -> pygame.Surface:
"""
Returns:
The image of the figure.
"""
return pygame.image.load(BASE_PATH / "assets" / "figures" / self.filename).convert_alpha()
def _load_image(filename: str) -> pygame.Surface:
return pygame.image.load(BASE_PATH / "assets" / "figures" / filename) # TODO: add `.convert_alpha()``
# TODO: change colors of images
class Figure(Enum):
@ -42,76 +39,76 @@ class Figure(Enum):
L: The L figure.
"""
I = FigureParams( # type: ignore
I = FigureConfig(
[
Vec2(0, 0),
Vec2(0, -1),
Vec2(0, -2),
Vec2(0, 1),
],
CONFIG.colors.cyan,
"I.png",
TokyoNightNight().cyan,
_load_image("I.png"),
)
O = FigureParams( # type: ignore
O = FigureConfig(
[
Vec2(0, 0),
Vec2(0, -1),
Vec2(1, 0),
Vec2(1, -1),
],
CONFIG.colors.yellow,
"O.png",
TokyoNightNight().yellow,
_load_image("O.png"),
)
T = FigureParams( # type: ignore
T = FigureConfig(
[
Vec2(0, 0),
Vec2(-1, 0),
Vec2(1, 0),
Vec2(0, -1),
],
CONFIG.colors.purple,
"T.png",
TokyoNightNight().purple,
_load_image("T.png"),
)
S = FigureParams( # type: ignore
S = FigureConfig(
[
Vec2(0, 0),
Vec2(-1, 0),
Vec2(0, -1),
Vec2(1, -1),
],
CONFIG.colors.green,
"S.png",
TokyoNightNight().green,
_load_image("S.png"),
)
Z = FigureParams( # type: ignore
Z = FigureConfig(
[
Vec2(0, 0),
Vec2(1, 0),
Vec2(0, -1),
Vec2(-1, -1),
],
CONFIG.colors.red,
"Z.png",
TokyoNightNight().red,
_load_image("Z.png"),
)
J = FigureParams( # type: ignore
J = FigureConfig(
[
Vec2(0, 0),
Vec2(0, -1),
Vec2(0, 1),
Vec2(-1, 1),
],
CONFIG.colors.blue,
"J.png",
TokyoNightNight().blue,
_load_image("J.png"),
)
L = FigureParams( # type: ignore
L = FigureConfig(
[
Vec2(0, 0),
Vec2(0, -1),
Vec2(0, 1),
Vec2(1, 1),
],
CONFIG.colors.orange,
"L.png",
TokyoNightNight().orange,
_load_image("L.png"),
)
@classmethod

Binary file not shown.

View File

@ -99,7 +99,7 @@ cd Tetris
3. Instalējiet nepieciešamās atkarības:
```bash
pip install -e .
pip install .
```
4. Palaidiet spēli:
@ -116,7 +116,7 @@ python -m tetris
=== Vispārīgi iestatījumi
#par(first-line-indent: 0cm, [
/ `pause`: definē taustiņu(-s), lai apturētu spēli.
/ `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>).
/ `quit`: definē taustiņu(-s), lai izietu no spēles.
/ `colorscheme`: norāda spēles saskarnes krāsu shēmu. Iespējas ietver:
- `tokyonight-day`
@ -140,7 +140,7 @@ python -m tetris
=== Papildus darbību iestatījumi
#par(first-line-indent: 0cm, [
/ `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>).
/ `hold`: definē taustiņu(-s), lai uzglabātu tetromino (WIP @WIP).
/ `drop`: definē taustiņ(-s), lai nekavējoties nomestu tetromino.
])