from textual.reactive import reactive from textual.widget import Widget from textual.app import ComposeResult from textual.message import Message from textual import events from datetime import datetime import random from asyncio import sleep from BingoField import BingoField 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) cursor_x, cursor_y = 2, 2 def __init__(self) -> None: '''Initialize the game''' self.fieldstate = [False for _ in range(25)] self.can_focus = True super().__init__() self.fields = [ 'Deko aufgehängt', '6 Stunden Schlaf', 'Tschunk getrunken', 'Spaß gehabt', 'Sticker getauscht', 'DECT genutzt', 'Hardware gehackt', 'Kabel vergessen', 'Halle gesucht', '$dinge gelötet', 'Code geschrieben', 'Neue Leute kennengelernt', 'Wasser getrunken', 'Waffel gegessen', 'Corona Test gemacht', '2 Mahlzeiten gegessen', 'Fairydust bewundert', 'Talk angeschaut', 'CCC Merch getragen', 'getrollt', 'In der Lounge getanzt', 'Etwas Neues ausprobiert', 'Maske getragen', 'geduscht', 'Gulasch gegessen' ] self.default_fields = self.fields self.roll_board(int(datetime.now().timestamp())) def fieldnum_from_cursor(self) -> int: '''Returns the list index position of the highlighted field''' return self.cursor_x + ( self.cursor_y * 5) 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 random.seed(seed) self.fields = random.sample(self.default_fields, len(self.fields)) 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)] for idx, field in enumerate(self.query(BingoField)): field.text = new_state[idx] def compose(self) -> ComposeResult: '''Create 25 bingo fields.''' for _ in range(25): yield BingoField(_, self.fields[_]) def on_focus(self, message: Message) -> None: '''Called when the BingoBoard receives focus. Enables cursor.''' fields = self.query(BingoField) fields[self.fieldnum_from_cursor()].set_highlighted(True) def on_blur(self, message: Message) -> None: '''Called when the BingoBoard loses focus. Disables cursor.''' fields = self.query(BingoField) fields[self.fieldnum_from_cursor()].set_highlighted(False) def on_key(self, event: events.Key) -> None: '''Handles keyboard input when BingoBoard is focused.''' fields = self.query(BingoField) fields[self.fieldnum_from_cursor()].set_highlighted(False) match event.key: case 'up': self.cursor_y -= 1 if self.cursor_y > 0 else 0 case 'down': self.cursor_y += 1 if self.cursor_y < 4 else 0 case 'left': self.cursor_x -= 1 if self.cursor_x > 0 else 0 case 'right': self.cursor_x += 1 if self.cursor_x < 4 else 0 case 'enter': fields[self.fieldnum_from_cursor()].on_click() fields[self.fieldnum_from_cursor()].set_highlighted(True) 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 if self.is_bingo(): self.screen.styles.animate("background", 'red', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'orange', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'yellow', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'green', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'lightblue', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'blue', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", 'purple', duration=0.25) await sleep(0.25) self.screen.styles.animate("background", '#1c1c1c', duration=0.25) 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)] # check rows bingo = any( [ all(row) for row in array ] ) # check cols bingo = bingo or any([ all(self.fieldstate[num::5]) for num in range(5)]) # check bottom left to upper right bingo = bingo or all([ self.fieldstate[4], self.fieldstate[8], self.fieldstate[12], self.fieldstate[16], self.fieldstate[20] ]) # check top left to bottom right bingo = bingo or all([ self.fieldstate[0], self.fieldstate[6], self.fieldstate[12], self.fieldstate[18], self.fieldstate[24] ]) return bingo