Skip to content

Commit c6aef4b

Browse files
committed
Added Merlin to examples
1 parent a85edb7 commit c6aef4b

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

examples/merlin.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from __future__ import annotations
2+
3+
import random
4+
from datetime import timedelta
5+
from time import monotonic
6+
7+
from textual import events
8+
from textual.app import App, ComposeResult
9+
from textual.containers import Grid
10+
from textual.reactive import var
11+
from textual.renderables.gradient import LinearGradient
12+
from textual.widget import Widget
13+
from textual.widgets import Digits, Label, Switch
14+
15+
# A nice rainbow of colors.
16+
COLORS = [
17+
"#881177",
18+
"#aa3355",
19+
"#cc6666",
20+
"#ee9944",
21+
"#eedd00",
22+
"#99dd55",
23+
"#44dd88",
24+
"#22ccbb",
25+
"#00bbcc",
26+
"#0099cc",
27+
"#3366bb",
28+
"#663399",
29+
]
30+
31+
32+
# Maps a switch number on to other switch numbers, which should be toggled.
33+
TOGGLES: dict[int, tuple[int, ...]] = {
34+
1: (2, 4, 5),
35+
2: (1, 3),
36+
3: (2, 5, 6),
37+
4: (1, 7),
38+
5: (2, 4, 6, 8),
39+
6: (3, 9),
40+
7: (4, 5, 8),
41+
8: (7, 9),
42+
9: (5, 6, 8),
43+
}
44+
45+
46+
class LabelSwitch(Widget):
47+
"""Switch with a numeric label."""
48+
49+
DEFAULT_CSS = """
50+
LabelSwitch Label {
51+
text-align: center;
52+
width: 1fr;
53+
text-style: bold;
54+
}
55+
56+
LabelSwitch Label#label-5 {
57+
color: $text-disabled;
58+
}
59+
"""
60+
61+
def __init__(self, switch_no: int) -> None:
62+
self.switch_no = switch_no
63+
super().__init__()
64+
65+
def compose(self) -> ComposeResult:
66+
"""Compose the label and a switch."""
67+
yield Label(str(self.switch_no), id=f"label-{self.switch_no}")
68+
yield Switch(id=f"switch-{self.switch_no}", name=str(self.switch_no))
69+
70+
71+
class Timer(Digits):
72+
"""Displays a timer that stops when you win."""
73+
74+
DEFAULT_CSS = """
75+
Timer {
76+
text-align: center;
77+
width: auto;
78+
margin: 2 8;
79+
color: $warning;
80+
}
81+
"""
82+
start_time = var(0.0)
83+
running = var(True)
84+
85+
def on_mount(self) -> None:
86+
"""Start the timer on mount."""
87+
self.start_time = monotonic()
88+
self.set_interval(1, self.tick)
89+
self.tick()
90+
91+
def tick(self) -> None:
92+
"""Called from `set_interval` to update the clock."""
93+
if self.start_time == 0 or not self.running:
94+
return
95+
time_elapsed = timedelta(seconds=int(monotonic() - self.start_time))
96+
self.update(str(time_elapsed))
97+
98+
99+
class MerlinApp(App):
100+
"""A simple reproduction of one game on the Merlin hand held console."""
101+
102+
CSS = """
103+
Screen {
104+
align: center middle;
105+
}
106+
107+
Screen.-win {
108+
background: transparent;
109+
}
110+
111+
Screen.-win Timer {
112+
color: $success;
113+
}
114+
115+
Grid {
116+
width: auto;
117+
height: auto;
118+
border: thick $primary;
119+
padding: 1 2;
120+
grid-size: 3 3;
121+
grid-rows: auto;
122+
grid-columns: auto;
123+
grid-gutter: 1 1;
124+
background: $surface;
125+
}
126+
"""
127+
128+
def render(self) -> LinearGradient:
129+
"""Renders a gradient, when the background is transparent."""
130+
stops = [(i / (len(COLORS) - 1), c) for i, c in enumerate(COLORS)]
131+
return LinearGradient(30.0, stops)
132+
133+
def compose(self) -> ComposeResult:
134+
"""Compose a timer, and a grid of 9 switches."""
135+
yield Timer()
136+
with Grid():
137+
for switch in (7, 8, 9, 4, 5, 6, 1, 2, 3):
138+
yield LabelSwitch(switch)
139+
140+
def on_mount(self) -> None:
141+
"""Randomize the switches on mount."""
142+
for switch_no in range(1, 10):
143+
if random.randint(0, 1):
144+
self.query_one(f"#switch-{switch_no}", Switch).toggle()
145+
146+
def check_win(self) -> bool:
147+
"""Check for a win."""
148+
on_switches = {
149+
int(switch.name or "0") for switch in self.query(Switch) if switch.value
150+
}
151+
return on_switches == {1, 2, 3, 4, 6, 7, 8, 9}
152+
153+
def on_switch_changed(self, event: Switch.Changed) -> None:
154+
"""Called when a switch is toggled."""
155+
# The switch that was pressed
156+
switch_no = int(event.switch.name or "0")
157+
# Also toggle corresponding switches
158+
with self.prevent(Switch.Changed):
159+
for toggle_no in TOGGLES[switch_no]:
160+
self.query_one(f"#switch-{toggle_no}", Switch).toggle()
161+
# Check the win
162+
if self.check_win():
163+
self.query_one("Screen").add_class("-win")
164+
self.query_one(Timer).running = False
165+
166+
def on_key(self, event: events.Key) -> None:
167+
"""Maps switches to keys, so we can use the keyboard as well."""
168+
if event.character and event.character.isdigit():
169+
self.query_one(f"#switch-{event.character}", Switch).toggle()
170+
171+
172+
if __name__ == "__main__":
173+
MerlinApp().run()

0 commit comments

Comments
 (0)