Compare commits
No commits in common. "master" and "1-reproducible-board" have entirely different histories.
master
...
1-reproduc
@ -1,24 +0,0 @@
|
|||||||
from textual.command import DiscoveryHit, Provider, Hits, Hit
|
|
||||||
|
|
||||||
class AboutCommand(Provider):
|
|
||||||
'''Class to represent the About command in the menu'''
|
|
||||||
|
|
||||||
async def discover(self) -> Hits:
|
|
||||||
yield DiscoveryHit(
|
|
||||||
display='About',
|
|
||||||
command=self.app.action_toggle_sidebar,
|
|
||||||
help='Link to repo etc.'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def search(self, query: str) -> Hits:
|
|
||||||
'''Called when the search functionality is used'''
|
|
||||||
matcher = self.matcher(query)
|
|
||||||
command = "About"
|
|
||||||
score = matcher.match(command)
|
|
||||||
if score > 0:
|
|
||||||
yield Hit(
|
|
||||||
score,
|
|
||||||
matcher.highlight(command),
|
|
||||||
self.app.action_toggle_sidebar,
|
|
||||||
'Link to repo etc.'
|
|
||||||
)
|
|
182
BingoBoard.py
182
BingoBoard.py
@ -1,182 +0,0 @@
|
|||||||
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
|
|
@ -1,54 +0,0 @@
|
|||||||
from textual.widgets import Input, Static, Button
|
|
||||||
from textual.containers import Horizontal
|
|
||||||
from textual.validation import Number
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from BingoBoard import BingoBoard
|
|
||||||
|
|
||||||
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:
|
|
||||||
'''Create child widgets for the app.'''
|
|
||||||
self.board = BingoBoard()
|
|
||||||
yield self.board
|
|
||||||
self.input_field = Input(
|
|
||||||
str(self.board.seed),
|
|
||||||
type='integer',
|
|
||||||
placeholder='UNIX timestamp',
|
|
||||||
max_length=10,
|
|
||||||
classes='seed_input',
|
|
||||||
validators=[
|
|
||||||
Number(minimum=1000000000, maximum = 2000000000)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.input_field.border_title = 'Seed'
|
|
||||||
yield Horizontal(
|
|
||||||
self.input_field,
|
|
||||||
Button.error(':game_die: re-roll', classes='roll_btn'),
|
|
||||||
classes='bottom_line'
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
'''Re-roll the board state with current time as seed'''
|
|
||||||
self.board.roll_board(int(datetime.now().timestamp()))
|
|
||||||
self.input_field.value = str(self.board.seed)
|
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
'''Re-roll the board state with the seed from the input'''
|
|
||||||
if event.validation_result.is_valid:
|
|
||||||
self.board.roll_board(int(event.value))
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
|||||||
from textual.widgets import Static
|
|
||||||
from textual.message import Message
|
|
||||||
from textual.reactive import reactive
|
|
||||||
|
|
||||||
class BingoField(Static):
|
|
||||||
'''
|
|
||||||
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('')
|
|
||||||
|
|
||||||
class Selected(Message):
|
|
||||||
'''Send message to the board containing clicked field info'''
|
|
||||||
def __init__(self, num: int, selected: bool) -> None:
|
|
||||||
self.num = num
|
|
||||||
self.selected = selected
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def __init__(self, num, text: str) -> None:
|
|
||||||
self.num = num
|
|
||||||
self.selected = False
|
|
||||||
self.highlighted = False
|
|
||||||
super().__init__()
|
|
||||||
self.text = text
|
|
||||||
|
|
||||||
def on_click(self) -> None:
|
|
||||||
'''Add CSS class field_selected to self and POST to BingoBoard'''
|
|
||||||
self.selected = not self.selected
|
|
||||||
if self.selected:
|
|
||||||
self.add_class('field_selected')
|
|
||||||
else:
|
|
||||||
self.remove_class('field_selected')
|
|
||||||
|
|
||||||
# The post_message method sends an event to be handled in the DOM
|
|
||||||
self.post_message(self.Selected(self.num, self.selected))
|
|
||||||
|
|
||||||
def set_highlighted(self, new_highlight: bool) -> None:
|
|
||||||
'''Add or remove the field_highlighted CSS class from self'''
|
|
||||||
self.highlighted = new_highlight
|
|
||||||
if self.highlighted:
|
|
||||||
self.add_class('field_highlighted')
|
|
||||||
else:
|
|
||||||
self.remove_class('field_highlighted')
|
|
||||||
|
|
||||||
def render(self) -> str:
|
|
||||||
'''Return the fields text'''
|
|
||||||
return str(self.text)
|
|
40
README.md
40
README.md
@ -1,40 +0,0 @@
|
|||||||
# bingo-cli - Play bingo in your terminal (or browser)
|
|
||||||
|
|
||||||
bingo-cli is a TUI to play Bingo, built using [Textual](https://textual.textualize.io/).
|
|
||||||
|
|
||||||
![bingo screenshot](img/bingo.png)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
- Mouse and keyboard controls
|
|
||||||
- Reproducible board states (by supplying UNIX timestamps)
|
|
||||||
- Playable directly in your terminal, remotely via SSH or web browser
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Install in python3 venv
|
|
||||||
|
|
||||||
```
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# test to see if it works:
|
|
||||||
python3 bingo.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Serve as a webpage
|
|
||||||
|
|
||||||
- Install requirements as above.
|
|
||||||
- Download the latest build of `ttyd`: https://github.com/tsl0922/ttyd/releases
|
|
||||||
```
|
|
||||||
chmod +x ttyd
|
|
||||||
# if not already in venv:
|
|
||||||
source venv/bin/activate
|
|
||||||
./ttyd -W ./bingo.py
|
|
||||||
```
|
|
||||||
- Bingo is now served on `http://localhost:7681`.
|
|
||||||
- Check `ttyd --help` for more options and consider building a reverse proxy and `systemd` service.
|
|
||||||
|
|
||||||
### Serve via SSH
|
|
||||||
|
|
||||||
TBA
|
|
31
Sidebar.py
31
Sidebar.py
@ -1,31 +0,0 @@
|
|||||||
from textual.containers import Container
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from textual.widgets import Static, Button
|
|
||||||
|
|
||||||
MESSAGE = '''
|
|
||||||
Be excellent to each other!
|
|
||||||
|
|
||||||
Made with :red_heart: by Panki
|
|
||||||
|
|
||||||
[@click="app.open_link('https://tty0.social/@panki')"]Mastodon:[/] tty0.social/@panki
|
|
||||||
[@click="app.open_link('https://git.theresno.cloud/panki/bingo-cli')"]Source:[/] git.theresno.cloud/panki/bingo-cli
|
|
||||||
|
|
||||||
Built using [@click="app.open_link('https://textual.textualize.io/')"]Textual[/]
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
class Sidebar(Container):
|
|
||||||
'''Class to represent the sidebar'''
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
'''Create the widgets that make up the sidebar'''
|
|
||||||
yield Static('CCC Bingo', classes='title')
|
|
||||||
yield Static(MESSAGE, classes='message')
|
|
||||||
self.button = Button('< Back', variant='primary', classes='btn_sidebar')
|
|
||||||
self.button.disabled = True
|
|
||||||
yield self.button
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
'''Closes the sidebar'''
|
|
||||||
self.app.action_toggle_sidebar()
|
|
||||||
|
|
218
bingo.py
218
bingo.py
@ -1,49 +1,197 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Header
|
from textual.widgets import Header, Input, Static, Button
|
||||||
|
from textual.widget import Widget
|
||||||
|
from textual.message import Message
|
||||||
|
from textual.containers import Horizontal
|
||||||
|
from textual.validation import Number
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
|
|
||||||
from BingoDisplay import BingoDisplay
|
from asyncio import sleep
|
||||||
from AboutCommand import AboutCommand
|
|
||||||
from Sidebar import Sidebar
|
|
||||||
|
|
||||||
class BingoApp(App):
|
from datetime import datetime
|
||||||
'''
|
import random
|
||||||
A Textual app to run a Bingo board.
|
|
||||||
|
|
||||||
Contains the sidebar, header, and BingoDisplay.
|
class BingoField(Static):
|
||||||
'''
|
"""A Bingo field widget."""
|
||||||
|
|
||||||
|
cursor_x, cursor_y = 2, 2
|
||||||
|
text = reactive("temp")
|
||||||
|
|
||||||
CSS_PATH = "bingo.tcss"
|
class Selected(Message):
|
||||||
COMMANDS = App.COMMANDS | {AboutCommand}
|
"""Send message to the board containing clicked field info"""
|
||||||
AUTO_FOCUS = 'Input'
|
def __init__(self, num: int, selected: bool) -> None:
|
||||||
|
self.num = num
|
||||||
|
self.selected = selected
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
show_sidebar = reactive(False)
|
def __init__(self, num, text: str) -> None:
|
||||||
|
self.num = num
|
||||||
def action_toggle_sidebar(self) -> None:
|
self.selected = False
|
||||||
'''Toggle the sidebar on or off'''
|
super().__init__()
|
||||||
sidebar = self.query_one(Sidebar)
|
self.text = text
|
||||||
self.set_focus(None)
|
|
||||||
if sidebar.has_class("-hidden"):
|
|
||||||
sidebar.remove_class("-hidden")
|
|
||||||
sidebar.button.disabled = False
|
|
||||||
else:
|
|
||||||
if sidebar.query("*:focus"):
|
|
||||||
self.screen.set_focus(None)
|
|
||||||
sidebar.add_class("-hidden")
|
|
||||||
sidebar.button.disabled = True
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
'''Create child widgets for the app.'''
|
|
||||||
yield Sidebar(classes="-hidden")
|
|
||||||
yield Header(show_clock=True)
|
|
||||||
yield BingoDisplay()
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
'''Set title of the app'''
|
self.styles.content_align = ("center", "middle")
|
||||||
self.title = 'CCC Bingo'
|
self.styles.border = ("solid", "green")
|
||||||
self.sub_title = 'GPN22 Edition'
|
self.styles.height = '100%'
|
||||||
|
self.styles.width = '100%'
|
||||||
|
|
||||||
|
def on_click(self) -> None:
|
||||||
|
self.selected = not self.selected
|
||||||
|
if self.selected:
|
||||||
|
self.styles.animate("background", 'green', duration=0.1)
|
||||||
|
self.styles.border = ("solid", "black")
|
||||||
|
else:
|
||||||
|
self.styles.animate("background", '#1c1c1c', duration=0.1)
|
||||||
|
self.styles.border = ("solid", "green")
|
||||||
|
|
||||||
|
# The post_message method sends an event to be handled in the DOM
|
||||||
|
self.post_message(self.Selected(self.num, self.selected))
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return str(self.text)
|
||||||
|
|
||||||
|
class BingoApp(App):
|
||||||
|
"""A Textual app to run a Bingo board."""
|
||||||
|
CSS_PATH = "bingo.tcss"
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create child widgets for the app."""
|
||||||
|
yield Header()
|
||||||
|
yield BingoDisplay()
|
||||||
|
|
||||||
|
def action_toggle_dark(self) -> None:
|
||||||
|
"""An action to toggle dark mode."""
|
||||||
|
self.dark = not self.dark
|
||||||
|
|
||||||
|
class BingoDisplay(Static):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create child widgets for the app."""
|
||||||
|
self.board = BingoBoard()
|
||||||
|
yield self.board
|
||||||
|
self.input_field = Input(
|
||||||
|
str(self.board.seed),
|
||||||
|
type='integer',
|
||||||
|
placeholder='UNIX timestamp',
|
||||||
|
max_length=10,
|
||||||
|
classes='seed_input',
|
||||||
|
validators=[
|
||||||
|
Number(minimum=1000000000, maximum = 2000000000)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.input_field.border_title = 'Seed'
|
||||||
|
yield Horizontal(
|
||||||
|
self.input_field,
|
||||||
|
Button.error(':game_die: re-roll', classes='roll_btn'),
|
||||||
|
classes='bottom_line'
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
""""""
|
||||||
|
# reroll
|
||||||
|
self.board.roll_board(int(datetime.now().timestamp()))
|
||||||
|
self.input_field.value = str(self.board.seed)
|
||||||
|
|
||||||
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
|
if event.validation_result.is_valid:
|
||||||
|
self.board.roll_board(int(event.value))
|
||||||
|
|
||||||
|
class BingoBoard(Widget):
|
||||||
|
|
||||||
|
fields = reactive([], recompose = True)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.fieldstate = [False for _ in range(25)]
|
||||||
|
super().__init__()
|
||||||
|
self.fields = [
|
||||||
|
'Datenelch',
|
||||||
|
'6 Stunden Schlaf',
|
||||||
|
'Tschunk getrunken',
|
||||||
|
'Spaß gehabt',
|
||||||
|
'Sticker getauscht',
|
||||||
|
'DECT genutzt',
|
||||||
|
'Hardware gehackt',
|
||||||
|
'Kabel vergessen',
|
||||||
|
'Halle gesucht',
|
||||||
|
'$dinge gelötet',
|
||||||
|
'an SoS teilgenommen',
|
||||||
|
'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 roll_board(self, seed):
|
||||||
|
self.seed = seed
|
||||||
|
random.seed(seed)
|
||||||
|
self.fields = random.sample(self.default_fields, len(self.fields))
|
||||||
|
|
||||||
|
def watch_fields(self, new_state) -> None:
|
||||||
|
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 child widgets for the app."""
|
||||||
|
for _ in range(25):
|
||||||
|
yield BingoField(_, self.fields[_])
|
||||||
|
|
||||||
|
async def on_bingo_field_selected(self, message: BingoField.Selected) -> None:
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = BingoApp()
|
app = BingoApp()
|
||||||
|
65
bingo.tcss
65
bingo.tcss
@ -1,30 +1,9 @@
|
|||||||
Screen {
|
|
||||||
layers: base overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
BingoBoard {
|
BingoBoard {
|
||||||
layout: grid;
|
layout: grid;
|
||||||
grid-size: 5 5;
|
grid-size: 5 5;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
BingoField {
|
|
||||||
content-align: center middle;
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background: $panel;
|
|
||||||
border: thick $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
BingoField.field_selected {
|
|
||||||
background: $success;
|
|
||||||
}
|
|
||||||
|
|
||||||
BingoField.field_highlighted {
|
|
||||||
color: $secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
BingoDisplay {
|
BingoDisplay {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -38,51 +17,21 @@ BingoDisplay {
|
|||||||
.seed_input {
|
.seed_input {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
border-title-align: left;
|
border-title-align: left;
|
||||||
background: $background;
|
|
||||||
}
|
}
|
||||||
.roll_btn {
|
.roll_btn {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
height: 100%;
|
||||||
|
border: solid green;
|
||||||
|
text-align: center;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
Input.-valid {
|
Input.-valid {
|
||||||
border: tall $success 60%;
|
border: tall $success 60%;
|
||||||
}
|
}
|
||||||
Input.-valid:focus {
|
Input.-valid:focus {
|
||||||
border: tall $success;
|
border: tall $success;
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar {
|
|
||||||
width: 40;
|
|
||||||
background: $panel;
|
|
||||||
transition: offset 500ms in_out_cubic;
|
|
||||||
layer: overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sidebar:focus-within {
|
|
||||||
offset: 0 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
Sidebar.-hidden {
|
|
||||||
offset-x: -100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
Static.title {
|
|
||||||
background: $boost;
|
|
||||||
color: $secondary;
|
|
||||||
padding: 2 4;
|
|
||||||
border-right: vkey $background;
|
|
||||||
dock: top;
|
|
||||||
text-align: center;
|
|
||||||
text-style: bold;
|
|
||||||
}
|
|
||||||
Static.message {
|
|
||||||
background: $boost;
|
|
||||||
color: $text;
|
|
||||||
height: 1fr;
|
|
||||||
border-right: vkey $background;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Button.btn_sidebar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
BIN
img/bingo.png
BIN
img/bingo.png
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Loading…
Reference in New Issue
Block a user