You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bingo-cli/BingoBoard.py

183 lines
6.5 KiB
Python

from textual.reactive import reactive
from textual.widget import Widget
from textual.app import ComposeResult
from textual.message import Message
5 months ago
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)]
5 months ago
self.can_focus = True
super().__init__()
self.fields = [
5 months ago
'Deko aufgehängt',
'6 Stunden Schlaf',
'Tschunk getrunken',
'Spaß gehabt',
'Sticker getauscht',
'DECT genutzt',
'Hardware gehackt',
'Kabel vergessen',
'Halle gesucht',
'$dinge gelötet',
5 months ago
'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)
5 months ago
def on_key(self, event: events.Key) -> None:
'''Handles keyboard input when BingoBoard is focused.'''
5 months ago
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
5 months ago
case 'enter':
fields[self.fieldnum_from_cursor()].on_click()
5 months ago
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