-
Notifications
You must be signed in to change notification settings - Fork 814
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a85edb7
commit c6aef4b
Showing
1 changed file
with
173 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
from __future__ import annotations | ||
|
||
import random | ||
from datetime import timedelta | ||
from time import monotonic | ||
|
||
from textual import events | ||
from textual.app import App, ComposeResult | ||
from textual.containers import Grid | ||
from textual.reactive import var | ||
from textual.renderables.gradient import LinearGradient | ||
from textual.widget import Widget | ||
from textual.widgets import Digits, Label, Switch | ||
|
||
# A nice rainbow of colors. | ||
COLORS = [ | ||
"#881177", | ||
"#aa3355", | ||
"#cc6666", | ||
"#ee9944", | ||
"#eedd00", | ||
"#99dd55", | ||
"#44dd88", | ||
"#22ccbb", | ||
"#00bbcc", | ||
"#0099cc", | ||
"#3366bb", | ||
"#663399", | ||
] | ||
|
||
|
||
# Maps a switch number on to other switch numbers, which should be toggled. | ||
TOGGLES: dict[int, tuple[int, ...]] = { | ||
1: (2, 4, 5), | ||
2: (1, 3), | ||
3: (2, 5, 6), | ||
4: (1, 7), | ||
5: (2, 4, 6, 8), | ||
6: (3, 9), | ||
7: (4, 5, 8), | ||
8: (7, 9), | ||
9: (5, 6, 8), | ||
} | ||
|
||
|
||
class LabelSwitch(Widget): | ||
"""Switch with a numeric label.""" | ||
|
||
DEFAULT_CSS = """ | ||
LabelSwitch Label { | ||
text-align: center; | ||
width: 1fr; | ||
text-style: bold; | ||
} | ||
LabelSwitch Label#label-5 { | ||
color: $text-disabled; | ||
} | ||
""" | ||
|
||
def __init__(self, switch_no: int) -> None: | ||
self.switch_no = switch_no | ||
super().__init__() | ||
|
||
def compose(self) -> ComposeResult: | ||
"""Compose the label and a switch.""" | ||
yield Label(str(self.switch_no), id=f"label-{self.switch_no}") | ||
yield Switch(id=f"switch-{self.switch_no}", name=str(self.switch_no)) | ||
|
||
|
||
class Timer(Digits): | ||
"""Displays a timer that stops when you win.""" | ||
|
||
DEFAULT_CSS = """ | ||
Timer { | ||
text-align: center; | ||
width: auto; | ||
margin: 2 8; | ||
color: $warning; | ||
} | ||
""" | ||
start_time = var(0.0) | ||
running = var(True) | ||
|
||
def on_mount(self) -> None: | ||
"""Start the timer on mount.""" | ||
self.start_time = monotonic() | ||
self.set_interval(1, self.tick) | ||
self.tick() | ||
|
||
def tick(self) -> None: | ||
"""Called from `set_interval` to update the clock.""" | ||
if self.start_time == 0 or not self.running: | ||
return | ||
time_elapsed = timedelta(seconds=int(monotonic() - self.start_time)) | ||
self.update(str(time_elapsed)) | ||
|
||
|
||
class MerlinApp(App): | ||
"""A simple reproduction of one game on the Merlin hand held console.""" | ||
|
||
CSS = """ | ||
Screen { | ||
align: center middle; | ||
} | ||
Screen.-win { | ||
background: transparent; | ||
} | ||
Screen.-win Timer { | ||
color: $success; | ||
} | ||
Grid { | ||
width: auto; | ||
height: auto; | ||
border: thick $primary; | ||
padding: 1 2; | ||
grid-size: 3 3; | ||
grid-rows: auto; | ||
grid-columns: auto; | ||
grid-gutter: 1 1; | ||
background: $surface; | ||
} | ||
""" | ||
|
||
def render(self) -> LinearGradient: | ||
"""Renders a gradient, when the background is transparent.""" | ||
stops = [(i / (len(COLORS) - 1), c) for i, c in enumerate(COLORS)] | ||
return LinearGradient(30.0, stops) | ||
|
||
def compose(self) -> ComposeResult: | ||
"""Compose a timer, and a grid of 9 switches.""" | ||
yield Timer() | ||
with Grid(): | ||
for switch in (7, 8, 9, 4, 5, 6, 1, 2, 3): | ||
yield LabelSwitch(switch) | ||
|
||
def on_mount(self) -> None: | ||
"""Randomize the switches on mount.""" | ||
for switch_no in range(1, 10): | ||
if random.randint(0, 1): | ||
self.query_one(f"#switch-{switch_no}", Switch).toggle() | ||
|
||
def check_win(self) -> bool: | ||
"""Check for a win.""" | ||
on_switches = { | ||
int(switch.name or "0") for switch in self.query(Switch) if switch.value | ||
} | ||
return on_switches == {1, 2, 3, 4, 6, 7, 8, 9} | ||
|
||
def on_switch_changed(self, event: Switch.Changed) -> None: | ||
"""Called when a switch is toggled.""" | ||
# The switch that was pressed | ||
switch_no = int(event.switch.name or "0") | ||
# Also toggle corresponding switches | ||
with self.prevent(Switch.Changed): | ||
for toggle_no in TOGGLES[switch_no]: | ||
self.query_one(f"#switch-{toggle_no}", Switch).toggle() | ||
# Check the win | ||
if self.check_win(): | ||
self.query_one("Screen").add_class("-win") | ||
self.query_one(Timer).running = False | ||
|
||
def on_key(self, event: events.Key) -> None: | ||
"""Maps switches to keys, so we can use the keyboard as well.""" | ||
if event.character and event.character.isdigit(): | ||
self.query_one(f"#switch-{event.character}", Switch).toggle() | ||
|
||
|
||
if __name__ == "__main__": | ||
MerlinApp().run() |