chore: remove unnecessary imports

This commit is contained in:
Kristofers Solo 2024-01-08 18:39:03 +02:00
parent 29cc83a2ac
commit e6a2b474e6
16 changed files with 124 additions and 127 deletions

View File

@ -69,6 +69,7 @@ extend-select = [
"TID", "TID",
"YTT", "YTT",
] ]
ignore = ["E741"]
show-fixes = true show-fixes = true
line-length = 120 line-length = 120
indent-width = 4 indent-width = 4
@ -86,3 +87,6 @@ skip-magic-trailing-comma = false
line-ending = "auto" line-ending = "auto"
docstring-code-format = true docstring-code-format = true
docstring-code-line-length = 40 docstring-code-line-length = 40
[tool.black]
line-length = 120

View File

@ -1,19 +1,23 @@
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
class BaseScreen(ABC, metaclass=ABCMeta): class BaseScreen(ABC, metaclass=ABCMeta):
"""Base screen class.""" """Base screen class."""
@abstractmethod @abstractmethod
def update(self, *args, **kwargs) -> None: def update(self, *args: Any, **kwargs: Any) -> None:
"""Update the screen.""" """Update the screen."""
@abstractmethod @abstractmethod
def draw(self, *args, **kwargs) -> None: def draw(self, *args: Any, **kwargs: Any) -> None:
"""Draw the screen.""" """Draw the screen."""
@abstractmethod @abstractmethod
def run(self, *args, **kwargs) -> None: def run(self, *args: Any, **kwargs: Any) -> None:
"""Run the screen.""" """Run the screen."""
@ -31,7 +35,7 @@ class SceenElement(ABC, metaclass=ABCMeta):
"""Initialize the surface.""" """Initialize the surface."""
@abstractmethod @abstractmethod
def _initialize_rect(self, *args, **kwargs) -> None: def _initialize_rect(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the rectangle.""" """Initialize the rectangle."""
@abstractmethod @abstractmethod
@ -51,5 +55,5 @@ class TextScreen(ABC, metaclass=ABCMeta):
"""Draw the text on the surface.""" """Draw the text on the surface."""
@abstractmethod @abstractmethod
def _display_text(self, *args, **kwargs) -> None: def _display_text(self, *args: Any, **kwargs: Any) -> None:
"""Display the text.""" """Display the text."""

View File

@ -1,5 +1,8 @@
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta, abstractmethod
from typing import Any, Callable, Optional from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Callable, Optional
import pygame import pygame
@ -7,9 +10,7 @@ import pygame
class BaseButton(ABC, metaclass=ABCMeta): class BaseButton(ABC, metaclass=ABCMeta):
"""Base button class.""" """Base button class."""
def __init__( def __init__(self, text: str, action: Optional[Callable[[], Optional[Any]]]) -> None:
self, text: str, action: Optional[Callable[[], Optional[Any]]]
) -> None:
self.action = action self.action = action
self.text = text self.text = text

View File

@ -1,4 +1,4 @@
from typing import Any, Callable, Optional from typing import TYPE_CHECKING
import pygame import pygame
from utils import CONFIG from utils import CONFIG
@ -6,11 +6,12 @@ from utils import CONFIG
from .base import BaseScreen, SceenElement, TextScreen from .base import BaseScreen, SceenElement, TextScreen
from .base_button import BaseButton from .base_button import BaseButton
if TYPE_CHECKING:
from typing import Any, Callable, Optional
class Button(BaseButton, BaseScreen, SceenElement, TextScreen): class Button(BaseButton, BaseScreen, SceenElement, TextScreen):
def __init__( def __init__(self, text: str, action: Optional[Callable[[], Optional[Any]]]) -> None:
self, text: str, action: Optional[Callable[[], Optional[Any]]]
) -> None:
super().__init__(text, action) super().__init__(text, action)
self._initialize_surface() self._initialize_surface()
self._initialize_font() self._initialize_font()
@ -19,11 +20,7 @@ class Button(BaseButton, BaseScreen, SceenElement, TextScreen):
def on_click(self, event: pygame.Event) -> None: def on_click(self, event: pygame.Event) -> None:
"""Handle click event.""" """Handle click event."""
if ( if event.type == pygame.MOUSEBUTTONDOWN and self.rect.collidepoint(event.pos) and self.action:
event.type == pygame.MOUSEBUTTONDOWN
and self.rect.collidepoint(event.pos)
and self.action
):
self.action() self.action()
def on_hover(self, event: pygame.Event) -> None: def on_hover(self, event: pygame.Event) -> None:

View File

@ -1,13 +1,18 @@
from typing import Any from typing import TYPE_CHECKING
import pygame import pygame
from utils import CONFIG, Figure, GameMode from utils import CONFIG, GameMode
from .base import BaseScreen from .base import BaseScreen
from .preview import Preview from .preview import Preview
from .score import Score from .score import Score
from .tetris import Tetris from .tetris import Tetris
if TYPE_CHECKING:
from typing import Any
from utils import Figure
class Game(BaseScreen): class Game(BaseScreen):
""" """
@ -71,9 +76,7 @@ class Game(BaseScreen):
self.clock = pygame.time.Clock() self.clock = pygame.time.Clock()
self.next_figure: Figure = self._generate_next_figure() self.next_figure: Figure = self._generate_next_figure()
self.tetris = Tetris( self.tetris = Tetris(self._get_next_figure, self._update_score, self.game_mode, self.settings)
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()
@ -110,10 +113,7 @@ class Game(BaseScreen):
def _start_background_music(self) -> None: def _start_background_music(self) -> None:
"""Start playing background music.""" """Start playing background music."""
if ( if self.game_mode is GameMode.PLAYER and self.settings["Volume"]["Music"]["enabled"]:
self.game_mode is GameMode.PLAYER
and self.settings["Volume"]["Music"]["enabled"]
):
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)

View File

@ -1,14 +1,19 @@
import sys import sys
from typing import Optional from typing import TYPE_CHECKING
import pygame import pygame
from loguru import logger from loguru import logger
from utils import CONFIG, GameMode, read_settings from utils import CONFIG, 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
if TYPE_CHECKING:
from typing import Optional
from utils import GameMode
class Main(BaseScreen, SceenElement, TextScreen): class Main(BaseScreen, SceenElement, TextScreen):
""" """
@ -45,9 +50,7 @@ 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 [ if event.key in [pygame.key.key_code(key) for key in self.settings["General"]["quit"]]:
pygame.key.key_code(key) for key in self.settings["General"]["quit"]
]:
self.exit() self.exit()
if not self.game: if not self.game:
@ -126,9 +129,9 @@ class Main(BaseScreen, SceenElement, TextScreen):
def _initialize_increment_height(self) -> None: def _initialize_increment_height(self) -> None:
"""Initialize the increment height for positioning text elements/buttons.""" """Initialize the increment height for positioning text elements/buttons."""
self.increment_height: float = ( self.increment_height: float = (self.display_surface.get_height() - CONFIG.window.size.height / 2) / len(
self.display_surface.get_height() - CONFIG.window.size.height / 2 self.buttons
) / len(self.buttons) )
def _display_text(self, text: str, pos: tuple[float, float]) -> None: def _display_text(self, text: str, pos: tuple[float, float]) -> None:
""" """
@ -156,8 +159,6 @@ class Main(BaseScreen, SceenElement, TextScreen):
for idx, button in enumerate(self.buttons): for idx, button in enumerate(self.buttons):
x = self.display_surface.get_width() / 2 x = self.display_surface.get_width() / 2
y = ( y = (
self.increment_height / 4 self.increment_height / 4 + idx * self.increment_height + CONFIG.window.size.height / 4
+ idx * self.increment_height
+ CONFIG.window.size.height / 4
) # TODO: tweak a bit more ) # TODO: tweak a bit more
button.draw(self.display_surface, (x, y)) button.draw(self.display_surface, (x, y))

View File

@ -1,8 +1,13 @@
from typing import TYPE_CHECKING
import pygame import pygame
from utils import CONFIG, Figure from utils import CONFIG
from .base import BaseScreen, SceenElement from .base import BaseScreen, SceenElement
if TYPE_CHECKING:
from utils import Figure
class Preview(BaseScreen, SceenElement): class Preview(BaseScreen, SceenElement):
""" """

View File

@ -118,9 +118,7 @@ class Score(BaseScreen, SceenElement, TextScreen):
self.surface.blit(value_surface, value_rect) self.surface.blit(value_surface, value_rect)
else: else:
text_surface = self.font.render( text_surface = self.font.render(f"{text}: {value}", True, CONFIG.colors.fg_sidebar)
f"{text}: {value}", True, CONFIG.colors.fg_sidebar
)
text_rect = text_surface.get_rect(center=pos) text_rect = text_surface.get_rect(center=pos)
self.surface.blit(text_surface, text_rect) self.surface.blit(text_surface, text_rect)
@ -148,9 +146,7 @@ class Score(BaseScreen, SceenElement, TextScreen):
def _initialize_rect(self) -> None: def _initialize_rect(self) -> None:
"""Initialize the score rectangle.""" """Initialize the score rectangle."""
self.rect = self.surface.get_rect( self.rect = self.surface.get_rect(bottomright=CONFIG.window.size - CONFIG.window.padding)
bottomright=CONFIG.window.size - CONFIG.window.padding
)
def _initialize_font(self) -> None: def _initialize_font(self) -> None:
"""Initialize the font used to display the score.""" """Initialize the font used to display the score."""

View File

@ -1,15 +1,20 @@
from typing import Any, Callable, Optional from typing import TYPE_CHECKING
import numpy as np import numpy as np
import pygame import pygame
from loguru import logger from loguru import logger
from utils import CONFIG, Direction, Figure, GameMode, Rotation from utils import CONFIG, Direction, Figure, GameMode, Rotation
from game.sprites import Block, Tetromino from game.sprites import Tetromino
from game.timer import Timer, Timers from game.timer import Timer, Timers
from .base import BaseScreen from .base import BaseScreen
if TYPE_CHECKING:
from typing import Any, Callable, Optional
from game.sprites import Block
class Tetris(BaseScreen): class Tetris(BaseScreen):
""" """
@ -161,9 +166,7 @@ class Tetris(BaseScreen):
""" """
return self.tetromino.drop() return self.tetromino.drop()
def create_new_tetromino( def create_new_tetromino(self, shape: Optional[Figure] = None) -> Optional[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()
self._check_finished_rows() self._check_finished_rows()
@ -377,19 +380,13 @@ class Tetris(BaseScreen):
def _initialize_sound(self) -> None: def _initialize_sound(self) -> None:
"""Initialize game sounds.""" """Initialize game sounds."""
if ( if self.game_mode is GameMode.PLAYER and self.settings["Volume"]["SFX"]["enabled"]:
self.game_mode is GameMode.PLAYER
and self.settings["Volume"]["SFX"]["enabled"]
):
self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing) self.landing_sound = pygame.mixer.Sound(CONFIG.music.landing)
self.landing_sound.set_volume(self.settings["Volume"]["SFX"]["level"]) self.landing_sound.set_volume(self.settings["Volume"]["SFX"]["level"])
def _play_landing_sound(self) -> None: def _play_landing_sound(self) -> None:
"""Play the landing sound effect.""" """Play the landing sound effect."""
if ( if self.game_mode is GameMode.PLAYER and self.settings["Volume"]["SFX"]["enabled"]:
self.game_mode is GameMode.PLAYER
and self.settings["Volume"]["SFX"]["enabled"]
):
self.landing_sound.play() self.landing_sound.play()
def _update_display_surface(self) -> None: def _update_display_surface(self) -> None:
@ -411,14 +408,10 @@ class Tetris(BaseScreen):
See `settings.toml` for the default key bindings. See `settings.toml` for the default key bindings.
""" """
right_keys: list[int] = [ right_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["right"]]
pygame.key.key_code(key) for key in self.settings["Movement"]["right"]
]
right_key_pressed = any(keys[key] for key in right_keys) right_key_pressed = any(keys[key] for key in right_keys)
left_keys: list[int] = [ left_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["left"]]
pygame.key.key_code(key) for key in self.settings["Movement"]["left"]
]
left_key_pressed = any(keys[key] for key in left_keys) left_key_pressed = any(keys[key] for key in left_keys)
if not self.timers.horizontal.active: if not self.timers.horizontal.active:
@ -435,14 +428,10 @@ class Tetris(BaseScreen):
See `settings.toml` for the default key bindings. See `settings.toml` for the default key bindings.
""" """
cw_keys: list[int] = [ cw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["cw"]]
pygame.key.key_code(key) for key in self.settings["Rotation"]["cw"]
]
cw_key_pressed = any(keys[key] for key in cw_keys) cw_key_pressed = any(keys[key] for key in cw_keys)
ccw_keys: list[int] = [ ccw_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Rotation"]["ccw"]]
pygame.key.key_code(key) for key in self.settings["Rotation"]["ccw"]
]
ccw_key_pressed = any(keys[key] for key in ccw_keys) ccw_key_pressed = any(keys[key] for key in ccw_keys)
if not self.timers.rotation.active: if not self.timers.rotation.active:
@ -460,9 +449,7 @@ class Tetris(BaseScreen):
See `settings.toml` for the default key bindings. See `settings.toml` for the default key bindings.
""" """
down_keys: list[int] = [ down_keys: list[int] = [pygame.key.key_code(key) for key in self.settings["Movement"]["down"]]
pygame.key.key_code(key) for key in self.settings["Movement"]["down"]
]
down_key_pressed = any(keys[key] for key in down_keys) down_key_pressed = any(keys[key] for key in 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
@ -478,9 +465,7 @@ class Tetris(BaseScreen):
See `settings.toml` for the default key bindings. See `settings.toml` for the default key bindings.
""" """
drop_keys = [ drop_keys = [pygame.key.key_code(key) for key in self.settings["Action"]["drop"]]
pygame.key.key_code(key) for key in self.settings["Action"]["drop"]
]
drop_key_pressed = any(keys[key] for key in drop_keys) drop_key_pressed = any(keys[key] for key in drop_keys)
if not self.timers.drop.active and drop_key_pressed: if not self.timers.drop.active and drop_key_pressed:

View File

@ -1,9 +1,13 @@
from typing import Any, Optional from typing import TYPE_CHECKING
import numpy as np
import pygame import pygame
from utils import CONFIG, Rotation from utils import CONFIG, Rotation
if TYPE_CHECKING:
from typing import Any, Optional
import numpy as np
class Block(pygame.sprite.Sprite): class Block(pygame.sprite.Sprite):
""" """
@ -24,7 +28,7 @@ class Block(pygame.sprite.Sprite):
self, self,
/, /,
*, *,
group: pygame.sprite.Group, group: pygame.sprite.Group, # type: ignore
pos: pygame.Vector2, pos: pygame.Vector2,
color: str, color: str,
phantom: bool = False, phantom: bool = False,
@ -42,9 +46,7 @@ class Block(pygame.sprite.Sprite):
self.pos.y * CONFIG.game.cell.width, self.pos.y * CONFIG.game.cell.width,
) )
def vertical_collision( def vertical_collision(self, x: int, field: np.ndarray[Optional["Block"], Any]) -> bool:
self, x: int, field: np.ndarray[Optional["Block"], Any]
) -> bool:
""" """
Checks for vertical collision with the game field. Checks for vertical collision with the game field.
@ -57,9 +59,7 @@ class Block(pygame.sprite.Sprite):
""" """
return not 0 <= x < CONFIG.game.columns or field[int(self.pos.y), x] return not 0 <= x < CONFIG.game.columns or field[int(self.pos.y), x]
def horizontal_collision( def horizontal_collision(self, y: int, field: np.ndarray[Optional["Block"], Any]) -> bool:
self, y: int, field: np.ndarray[Optional["Block"], Any]
) -> bool:
""" """
Checks for horizontal collision with the game field. Checks for horizontal collision with the game field.

View File

@ -1,10 +1,14 @@
from typing import Any, Callable, Optional from typing import TYPE_CHECKING
import numpy as np
import pygame import pygame
from utils import CONFIG, Direction, Figure, Rotation from utils import CONFIG, Direction, Figure, Rotation
from .block import Block if TYPE_CHECKING:
from typing import Any, Callable, Optional
import numpy as np
from .block import Block
class Tetromino: class Tetromino:
@ -28,7 +32,7 @@ class Tetromino:
def __init__( def __init__(
self, self,
group: pygame.sprite.Group, group: pygame.sprite.Group, # type: ignore
create_new: Optional[Callable[[Optional[Figure]], Optional["Tetromino"]]], create_new: Optional[Callable[[Optional[Figure]], Optional["Tetromino"]]],
field: np.ndarray[Optional[Block], Any], field: np.ndarray[Optional[Block], Any],
shape: Optional[Figure] = None, shape: Optional[Figure] = None,
@ -98,9 +102,7 @@ class Tetromino:
pivot: pygame.Vector2 = self.blocks[0].pos pivot: pygame.Vector2 = self.blocks[0].pos
for _ in range(3): for _ in range(3):
new_positions: list[pygame.Vector2] = [ new_positions: list[pygame.Vector2] = [block.rotate(pivot, rotation) for block in self.blocks]
block.rotate(pivot, rotation) for block in self.blocks
]
if self._are_new_positions_valid(new_positions): if self._are_new_positions_valid(new_positions):
self.update_block_positions(new_positions) self.update_block_positions(new_positions)
@ -142,13 +144,11 @@ class Tetromino:
True if there is a collision, False otherwise. True if there is a collision, False otherwise.
""" """
return self._check_horizontal_collision( return self._check_horizontal_collision(self.blocks, direction) or self._check_vertical_collision(
self.blocks, direction self.blocks, direction
) or self._check_vertical_collision(self.blocks, direction) )
def _check_vertical_collision( def _check_vertical_collision(self, blocks: list[Block], direction: Direction) -> bool:
self, blocks: list[Block], direction: Direction
) -> bool:
""" """
Checks for vertical collision. Checks for vertical collision.
@ -159,14 +159,9 @@ class Tetromino:
Returns: Returns:
True if there is a vertical collision, False otherwise. True if there is a vertical collision, False otherwise.
""" """
return any( return any(block.vertical_collision(int(block.pos.x + direction.value), self.field) for block in self.blocks)
block.vertical_collision(int(block.pos.x + direction.value), self.field)
for block in self.blocks
)
def _check_horizontal_collision( def _check_horizontal_collision(self, blocks: list[Block], direction: Direction) -> bool:
self, blocks: list[Block], direction: Direction
) -> bool:
""" """
Checks for horizontal collision. Checks for horizontal collision.
@ -177,10 +172,7 @@ class Tetromino:
Returns: Returns:
True if there is a horizontal collision, False otherwise. True if there is a horizontal collision, False otherwise.
""" """
return any( return any(block.horizontal_collision(int(block.pos.y + direction.value), self.field) for block in self.blocks)
block.horizontal_collision(int(block.pos.y + direction.value), self.field)
for block in self.blocks
)
def update_block_positions(self, new_positions: list[pygame.Vector2]) -> None: def update_block_positions(self, new_positions: list[pygame.Vector2]) -> None:
""" """
@ -209,7 +201,10 @@ class Tetromino:
for pos in new_positions for pos in new_positions
) )
def _initialize_blocks(self, group: pygame.sprite.Group) -> list[Block]: def _initialize_blocks(
self,
group: pygame.sprite.Group, # type: ignore
) -> list[Block]:
""" """
Initializes Tetromino blocks. Initializes Tetromino blocks.
@ -219,10 +214,7 @@ class Tetromino:
Returns: Returns:
List of initialized blocks. List of initialized blocks.
""" """
return [ return [Block(group=group, pos=pos, color=self.color, phantom=self.phantom) for pos in self.block_positions]
Block(group=group, pos=pos, color=self.color, phantom=self.phantom)
for pos in self.block_positions
]
def _generate_figure(self, shape: Optional[Figure]) -> Figure: def _generate_figure(self, shape: Optional[Figure]) -> Figure:
""" """

View File

@ -1,8 +1,11 @@
from typing import Any, Callable, NamedTuple, Optional from typing import TYPE_CHECKING
import pygame import pygame
from attrs import define, field from attrs import define, field
if TYPE_CHECKING:
from typing import Any, Callable, NamedTuple, Optional
@define @define
class Timer: class Timer:

View File

@ -1,7 +1,6 @@
from pathlib import Path from typing import TYPE_CHECKING
from attr import define from attr import define
from pygame import Vector2 as Vec2
from .colors import COLOR_DICT, TokyoNightNight from .colors import COLOR_DICT, TokyoNightNight
from .colors.tokyonight.base import Color from .colors.tokyonight.base import Color
@ -9,6 +8,12 @@ from .path import BASE_PATH
from .settings import read_settings from .settings import read_settings
from .tuples import Size from .tuples import Size
if TYPE_CHECKING:
from pathlib import Path
from pygame import Vector2 as Vec2
PADDING = 20 PADDING = 20
@ -139,9 +144,7 @@ class Config:
window: Window = Window() window: Window = Window()
font: Font = Font() font: Font = Font()
music: Music = Music() music: Music = Music()
colors: Color = COLOR_DICT.get( colors: Color = COLOR_DICT.get(read_settings()["General"]["colorscheme"], TokyoNightNight)()
read_settings()["General"]["colorscheme"], TokyoNightNight
)()
CONFIG = Config() CONFIG = Config()

View File

@ -1,13 +1,15 @@
import random import random
from enum import Enum from enum import Enum
from typing import NamedTuple from typing import TYPE_CHECKING, NamedTuple
import pygame import pygame
from pygame import Vector2 as Vec2
from .colors import TokyoNightNight from .colors import TokyoNightNight
from .path import BASE_PATH from .path import BASE_PATH
if TYPE_CHECKING:
from pygame import Vector2 as Vec2
class FigureConfig(NamedTuple): class FigureConfig(NamedTuple):
""" """
@ -23,9 +25,7 @@ class FigureConfig(NamedTuple):
def _load_image(filename: str) -> pygame.Surface: def _load_image(filename: str) -> pygame.Surface:
return pygame.image.load( return pygame.image.load(BASE_PATH / "assets" / "figures" / filename) # TODO: add `.convert_alpha()``
BASE_PATH / "assets" / "figures" / filename
) # TODO: add `.convert_alpha()``
# TODO: change colors of images # TODO: change colors of images

View File

@ -1,11 +1,14 @@
from pathlib import Path from typing import TYPE_CHECKING
from typing import Any
import toml import toml
from loguru import logger from loguru import logger
from .path import BASE_PATH from .path import BASE_PATH
if TYPE_CHECKING:
from pathlib import Path
from typing import Any
def save_settings(settings: dict[str, Any], file_path: Path) -> None: def save_settings(settings: dict[str, Any], file_path: Path) -> None:
""" """

View File

@ -1,4 +1,7 @@
from typing import NamedTuple, Union from typing import TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
from typing import Union
class Size(NamedTuple): class Size(NamedTuple):