docstrings, type hints

master
Felix Pankratz 5 months ago
parent 292bb56a8f
commit 2879d2abb1

@ -11,12 +11,36 @@ from asyncio import sleep
from BingoField import BingoField from BingoField import BingoField
class BingoBoard(Widget): class BingoBoard(Widget):
'''
A widget to display the fields of the bingo board.
Attributes
----------
fieldstate : list
A list of booleans indicating the selection state of each field
fields : reactive(list)
List of strings containing the actual text for each field
default_fields : list
List of field strings in default order, required for reproducability
seed : int
The seed used for the current board
Methods
-------
roll_board(seed):
Shuffle the fields according to the field and reset the board.
fieldnum_from_cursor():
Get the list index of the currently highlighted cell
is_bingo():
Check if there is a bingo.
'''
fields = reactive([], recompose = True) fields = reactive([], recompose = True)
cursor_x, cursor_y = 2, 2 cursor_x, cursor_y = 2, 2
def __init__(self) -> None: def __init__(self) -> None:
'''Initialize the game'''
self.fieldstate = [False for _ in range(25)] self.fieldstate = [False for _ in range(25)]
self.can_focus = True self.can_focus = True
super().__init__() super().__init__()
@ -51,32 +75,48 @@ class BingoBoard(Widget):
self.roll_board(int(datetime.now().timestamp())) self.roll_board(int(datetime.now().timestamp()))
def fieldnum_from_cursor(self) -> int: def fieldnum_from_cursor(self) -> int:
'''Returns the list index position of the highlighted field'''
return self.cursor_x + ( self.cursor_y * 5) return self.cursor_x + ( self.cursor_y * 5)
def roll_board(self, seed): def roll_board(self, seed: int) -> None:
'''
Roll the board according to the seed. Board updated through reactivity.
Parameters:
seed (int): A UNIX timestamp used as RNG seed
'''
self.seed = seed self.seed = seed
random.seed(seed) random.seed(seed)
self.fields = random.sample(self.default_fields, len(self.fields)) self.fields = random.sample(self.default_fields, len(self.fields))
def watch_fields(self, new_state) -> None: def watch_fields(self, new_state) -> None:
'''
Triggered when fields change (see roll_board).
Parameters:
new_state (list): The field strings in newly shuffled order
'''
self.fieldstate = [False for _ in range(25)] self.fieldstate = [False for _ in range(25)]
for idx, field in enumerate(self.query(BingoField)): for idx, field in enumerate(self.query(BingoField)):
field.text = new_state[idx] field.text = new_state[idx]
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Create child widgets for the app.""" '''Create 25 bingo fields.'''
for _ in range(25): for _ in range(25):
yield BingoField(_, self.fields[_]) yield BingoField(_, self.fields[_])
def on_focus(self, message: Message) -> None: def on_focus(self, message: Message) -> None:
'''Called when the BingoBoard receives focus. Enables cursor.'''
fields = self.query(BingoField) fields = self.query(BingoField)
fields[self.fieldnum_from_cursor()].set_highlighted(True) fields[self.fieldnum_from_cursor()].set_highlighted(True)
def on_blur(self, message: Message) -> None: def on_blur(self, message: Message) -> None:
'''Called when the BingoBoard loses focus. Disables cursor.'''
fields = self.query(BingoField) fields = self.query(BingoField)
fields[self.fieldnum_from_cursor()].set_highlighted(False) fields[self.fieldnum_from_cursor()].set_highlighted(False)
def on_key(self, event: events.Key) -> None: def on_key(self, event: events.Key) -> None:
'''Handles keyboard input when BingoBoard is focused.'''
fields = self.query(BingoField) fields = self.query(BingoField)
fields[self.fieldnum_from_cursor()].set_highlighted(False) fields[self.fieldnum_from_cursor()].set_highlighted(False)
match event.key: match event.key:
@ -94,6 +134,10 @@ class BingoBoard(Widget):
async def on_bingo_field_selected(self, message: BingoField.Selected) -> None: async def on_bingo_field_selected(self, message: BingoField.Selected) -> None:
'''
Triggered by child when it is clicked.
Updates fieldstate and checks for bingo, possibly triggering win animation.
'''
self.fieldstate[message.num] = message.selected self.fieldstate[message.num] = message.selected
if self.is_bingo(): if self.is_bingo():
self.screen.styles.animate("background", 'red', duration=0.25) self.screen.styles.animate("background", 'red', duration=0.25)
@ -112,7 +156,8 @@ class BingoBoard(Widget):
await sleep(0.25) await sleep(0.25)
self.screen.styles.animate("background", '#1c1c1c', duration=0.25) self.screen.styles.animate("background", '#1c1c1c', duration=0.25)
def is_bingo(self): def is_bingo(self) -> bool:
'''Check the board for a bingo (5 in a row).'''
array = [ self.fieldstate[i:i+5] for i in range(0, len(self.fieldstate), 5)] array = [ self.fieldstate[i:i+5] for i in range(0, len(self.fieldstate), 5)]
# check rows # check rows
bingo = any( [ all(row) for row in array ] ) bingo = any( [ all(row) for row in array ] )

@ -3,7 +3,25 @@ from textual.message import Message
from textual.reactive import reactive from textual.reactive import reactive
class BingoField(Static): class BingoField(Static):
'''A Bingo field widget.''' '''
A Bingo field widget.
Attributes
----------
text : str
The text displayed in the field
num : int
The running number of this field (0-24)
selected : bool
Wether the bingo field has been checked or not
highlighted : bool
Wether the cursor is currently on this field or not
Methods
-------
set_highlighted(new_highlight):
Sets the highlighted state (cursor) of this field
'''
text = reactive('') text = reactive('')
@ -22,6 +40,7 @@ class BingoField(Static):
self.text = text self.text = text
def on_click(self) -> None: def on_click(self) -> None:
'''Add CSS class field_selected to self and POST to BingoBoard'''
self.selected = not self.selected self.selected = not self.selected
if self.selected: if self.selected:
self.add_class('field_selected') self.add_class('field_selected')
@ -32,6 +51,7 @@ class BingoField(Static):
self.post_message(self.Selected(self.num, self.selected)) self.post_message(self.Selected(self.num, self.selected))
def set_highlighted(self, new_highlight: bool) -> None: def set_highlighted(self, new_highlight: bool) -> None:
'''Add or remove the field_highlighted CSS class from self'''
self.highlighted = new_highlight self.highlighted = new_highlight
if self.highlighted: if self.highlighted:
self.add_class('field_highlighted') self.add_class('field_highlighted')
@ -39,4 +59,5 @@ class BingoField(Static):
self.remove_class('field_highlighted') self.remove_class('field_highlighted')
def render(self) -> str: def render(self) -> str:
'''Return the fields text'''
return str(self.text) return str(self.text)

@ -5,7 +5,6 @@ from textual.widgets import Header, Input, Static, Button
from textual.containers import Horizontal, Container from textual.containers import Horizontal, Container
from textual.validation import Number from textual.validation import Number
from textual.command import DiscoveryHit, Provider, Hits, Hit from textual.command import DiscoveryHit, Provider, Hits, Hit
#from textual.command import Provider, Hits, Hit
from textual.reactive import reactive from textual.reactive import reactive
from datetime import datetime from datetime import datetime
@ -25,14 +24,16 @@ Built using [@click="app.open_link('https://textual.textualize.io/')"]Textual[/]
''' '''
class AboutCommand(Provider): class AboutCommand(Provider):
'''Class to represent the About command in the menu'''
async def discover(self) -> Hits: async def discover(self) -> Hits:
yield DiscoveryHit(display='About', command=self.app.action_toggle_sidebar, help='Link to repo etc.') yield DiscoveryHit(display='About', command=self.app.action_toggle_sidebar, help='Link to repo etc.')
#yield Hit(1, 'About', self.app.action_toggle_sidebar, help='Link to repo etc.')
def show_about(self): def show_about(self) -> None:
pass pass
async def search(self, query: str) -> Hits: async def search(self, query: str) -> Hits:
'''Called when the search functionality is used'''
matcher = self.matcher(query) matcher = self.matcher(query)
command = "About" command = "About"
score = matcher.match(command) score = matcher.match(command)
@ -40,7 +41,10 @@ class AboutCommand(Provider):
yield Hit(score, matcher.highlight(command), self.app.action_toggle_sidebar, 'Link to repo etc.') yield Hit(score, matcher.highlight(command), self.app.action_toggle_sidebar, 'Link to repo etc.')
class Sidebar(Container): class Sidebar(Container):
'''Class to represent the sidebar'''
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
'''Create the widgets that make up the sidebar'''
yield Static('CCC Bingo', classes='title') yield Static('CCC Bingo', classes='title')
yield Static(MESSAGE, classes='message') yield Static(MESSAGE, classes='message')
self.button = Button('< Back', variant='primary', classes='btn_sidebar') self.button = Button('< Back', variant='primary', classes='btn_sidebar')
@ -48,11 +52,16 @@ class Sidebar(Container):
yield self.button yield self.button
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
print('sidebar button press') '''Closes the sidebar'''
self.app.action_toggle_sidebar() self.app.action_toggle_sidebar()
class BingoApp(App): class BingoApp(App):
'''A Textual app to run a Bingo board.''' '''
A Textual app to run a Bingo board.
Contains the sidebar, header, and BingoDisplay.
'''
CSS_PATH = "bingo.tcss" CSS_PATH = "bingo.tcss"
COMMANDS = App.COMMANDS | {AboutCommand} COMMANDS = App.COMMANDS | {AboutCommand}
AUTO_FOCUS = 'Input' AUTO_FOCUS = 'Input'
@ -60,7 +69,7 @@ class BingoApp(App):
show_sidebar = reactive(False) show_sidebar = reactive(False)
def action_toggle_sidebar(self) -> None: def action_toggle_sidebar(self) -> None:
print('sidebar toggle') '''Toggle the sidebar on or off'''
sidebar = self.query_one(Sidebar) sidebar = self.query_one(Sidebar)
self.set_focus(None) self.set_focus(None)
if sidebar.has_class("-hidden"): if sidebar.has_class("-hidden"):
@ -79,14 +88,24 @@ class BingoApp(App):
yield BingoDisplay() yield BingoDisplay()
def on_mount(self) -> None: def on_mount(self) -> None:
'''Set title of the app'''
self.title = 'CCC Bingo' self.title = 'CCC Bingo'
self.sub_title = 'GPN22 Edition' self.sub_title = 'GPN22 Edition'
def action_toggle_dark(self) -> None:
'''An action to toggle dark mode.'''
self.dark = not self.dark
class BingoDisplay(Static): class BingoDisplay(Static):
'''
A Widget to represent the bingo UI.
Contains the board, input field and re-roll button.
Attributes
----------
board : BingoBoard
The BingoBoard object
input_field : Input
User input for game seed
'''
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
'''Create child widgets for the app.''' '''Create child widgets for the app.'''
self.board = BingoBoard() self.board = BingoBoard()

Loading…
Cancel
Save