Skip to content

Commit f89477f

Browse files
authored
Merge pull request #291 from dannybirds/master
Initial commit of Oblique Strategies, with readme and video.
2 parents 8686735 + 189ca97 commit f89477f

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import thumby
2+
import random
3+
import time
4+
5+
# Initialize the display
6+
thumby.display.setFPS(30)
7+
thumby.display.fill(0)
8+
9+
# Game configuration - change this title to customize your card app
10+
APP_TITLE = "Oblique Strategies"
11+
12+
# Constants
13+
SCREEN_WIDTH = 72
14+
SCREEN_HEIGHT = 40
15+
SCROLL_DELAY = 8 # Frames between scrolling action
16+
SCROLL_SPEED = 1 # Pixels to scroll per action
17+
FADE_STEPS = 8 # Number of steps in the fade animation
18+
19+
TITLE_BG = 0
20+
TITLE_FG = 1
21+
22+
CARD_BG = 1
23+
CARD_FG = 0
24+
25+
SMALL_FONT = "/lib/font3x5.bin"
26+
SMALL_FONT_WIDTH = 3
27+
SMALL_FONT_HEIGHT = 5
28+
29+
BIG_FONT = "/lib/font5x7.bin"
30+
BIG_FONT_WIDTH = 5
31+
BIG_FONT_HEIGHT = 7
32+
33+
FONT_SPACING = 1
34+
35+
LINE_HEIGHT = BIG_FONT_HEIGHT + 1
36+
MAX_VISIBLE_LINES = SCREEN_HEIGHT // LINE_HEIGHT
37+
38+
phrases = [
39+
"Abandon normal instruments",
40+
"Accept advice",
41+
"Accretion",
42+
"A line has two sides",
43+
"Allow an easement (an easement is the abandonment of a stricture)",
44+
"Are there sections? Consider transitions",
45+
"Ask people to work against their better judgment",
46+
"Ask your body",
47+
"Assemble some of the instruments in a group and treat the group",
48+
"Balance the consistency principle with the inconsistency principle",
49+
"Be dirty",
50+
"Breathe more deeply",
51+
"Bridges -build -burn",
52+
"Cascades",
53+
"Change instrument roles",
54+
"Change nothing and continue with immaculate consistency",
55+
"Children's voices -speaking -singing",
56+
"Cluster analysis",
57+
"Consider different fading systems",
58+
"Consult other sources -promising -unpromising",
59+
"Convert a melodic element into a rhythmic element",
60+
"Courage!",
61+
"Cut a vital connection",
62+
"Decorate, decorate",
63+
"Define an area as `safe` and use it as an anchor",
64+
"Destroy -nothing -the most important thing",
65+
"Discard an axiom",
66+
"Disconnect from desire",
67+
"Discover the recipes you are using and abandon them",
68+
"Distorting time",
69+
"Do nothing for as long as possible",
70+
"Don't be afraid of things because they're easy to do",
71+
"Don't be frightened of cliches",
72+
"Don't be frightened to display your talents",
73+
"Don't break the silence",
74+
"Don't stress one thing more than another",
75+
"Do something boring",
76+
"Do the washing up",
77+
"Do the words need changing?",
78+
"Do we need holes?",
79+
"Emphasize differences",
80+
"Emphasize repetitions",
81+
"Emphasize the flaws",
82+
"Faced with a choice, do both",
83+
"Feedback recordings into an acoustic situation",
84+
"Fill every beat with something",
85+
"Get your neck massaged",
86+
"Ghost echoes",
87+
"Give the game away",
88+
"Give way to your worst impulse",
89+
"Go slowly all the way round the outside",
90+
"Honor thy error as a hidden intention",
91+
"How would you have done it?",
92+
"Humanize something free of error",
93+
"Imagine the music as a moving chain or caterpillar",
94+
"Imagine the music as a set of disconnected events",
95+
"Infinitesimal gradations",
96+
"Intentions -credibility of -nobility of -humility of",
97+
"Into the impossible",
98+
"Is it finished?",
99+
"Is there something missing?",
100+
"Is the tuning appropriate?",
101+
"Just carry on",
102+
"Left channel, right channel, center channel",
103+
"Listen in total darkness, or in a very large room, very quietly",
104+
"Listen to the quiet voice",
105+
"Look at a very small object; look at its center",
106+
"Look at the order in which you do things",
107+
"Look closely at the most embarrassing details and amplify them",
108+
"Lowest common denominator check -single beat -single note -single riff",
109+
"Make a blank valuable by putting it in an exquisite frame",
110+
"Make an exhaustive list of everything you might do and do the last thing on the list",
111+
"Make a sudden, destructive, unpredictable action; incorporate",
112+
"Mechanicalize something idiosyncratic",
113+
"Mute and continue",
114+
"Only one element of each kind",
115+
"(Organic) machinery",
116+
"Overtly resist change",
117+
"Put in earplugs",
118+
"Remember those quiet evenings",
119+
"Remove ambiguities and convert to specifics",
120+
"Remove specifics and convert to ambiguities",
121+
"Repetition is a form of change",
122+
"Reverse",
123+
"Shortcircuit: (example: a man eating peas with the idea that they will improve his virility shovels them straight into his lap)",
124+
"Shut the door and listen from outside",
125+
"Simple subtraction",
126+
"Spectrum analysis",
127+
"Take a break",
128+
"Take away the elements in order of apparent non-importance",
129+
"Tape your mouth",
130+
"The inconsistency principle",
131+
"The tape is now the music",
132+
"Think of the radio",
133+
"Tidy up",
134+
"Trust in the you of now",
135+
"Turn it upside down",
136+
"Twist the spine",
137+
"Use an old idea",
138+
"Use an unacceptable color",
139+
"Use fewer notes",
140+
"Use filters",
141+
"Use 'unqualified' people",
142+
"Water",
143+
"What are you really thinking about just now?",
144+
"What is the reality of the situation?",
145+
"What mistakes did you make last time?",
146+
"What would your closest friend do?",
147+
"What wouldn't you do?",
148+
"Work at a different speed",
149+
"You are an engineer",
150+
"You can only make one dot at a time",
151+
"You don't have to be ashamed of using your own ideas"
152+
]
153+
154+
# App state
155+
current_phrase = ""
156+
scroll_position = 0
157+
scroll_counter = 0
158+
lines = []
159+
total_height = 0
160+
needs_scrolling = False
161+
is_fading = False
162+
fade_step = 0
163+
fade_in = False # False for fade out, True for fade in
164+
show_title_screen = True
165+
166+
def line_width(line: str) -> int:
167+
return len(line) * (BIG_FONT_WIDTH + FONT_SPACING)
168+
169+
def make_lines(text: str) -> tuple[list[str], int]:
170+
words = text.split(' ')
171+
lines = []
172+
current_line = ""
173+
174+
total_width = 0
175+
lines: list[str] = []
176+
for word in words:
177+
test_line = current_line + word + " "
178+
if line_width(test_line) <= SCREEN_WIDTH:
179+
current_line = test_line
180+
else:
181+
lines.append(current_line.strip())
182+
total_width = max(total_width, line_width(current_line.strip()))
183+
current_line = word + " "
184+
185+
if current_line: # Add the last line
186+
lines.append(current_line.strip())
187+
total_width = max(total_width, line_width(current_line.strip()))
188+
189+
return (lines, total_width)
190+
191+
def draw_title_screen():
192+
thumby.display.fill(TITLE_BG)
193+
194+
# Draw the app title centered (normal size, black text)
195+
thumby.display.setFont(BIG_FONT, BIG_FONT_WIDTH, BIG_FONT_HEIGHT, FONT_SPACING)
196+
title_lines, title_width = make_lines(APP_TITLE)
197+
title_x = max(1, (SCREEN_WIDTH - title_width) // 2)
198+
current_y = 1
199+
for line in title_lines:
200+
thumby.display.drawText(line, title_x, current_y, TITLE_FG)
201+
current_y += BIG_FONT_HEIGHT + 1
202+
203+
# Draw decorative line
204+
current_y += 1
205+
thumby.display.drawLine(5, current_y, SCREEN_WIDTH - 5, current_y, TITLE_FG)
206+
207+
# Draw instructions in smaller text
208+
thumby.display.setFont(SMALL_FONT, SMALL_FONT_WIDTH, SMALL_FONT_HEIGHT, FONT_SPACING)
209+
current_y += 3
210+
thumby.display.drawText("CONTROLS:", 13, current_y, TITLE_FG)
211+
current_y += SMALL_FONT_HEIGHT + 2
212+
thumby.display.drawText("A/B: New Card", 8, current_y, TITLE_FG)
213+
current_y += SMALL_FONT_HEIGHT + 2
214+
thumby.display.drawText("U/D: Scroll", 11, current_y, TITLE_FG)
215+
216+
thumby.display.update()
217+
218+
def start_fade_transition():
219+
global is_fading, fade_step, fade_in
220+
is_fading = True
221+
fade_step = 0
222+
fade_in = False # Start with fade out
223+
224+
225+
def select_random_phrase():
226+
global current_phrase, scroll_position, scroll_counter, lines, total_height, total_width, needs_scrolling
227+
228+
current_phrase = random.choice(phrases)
229+
scroll_position = 0
230+
scroll_counter = 0
231+
232+
# Split the phrase into lines to fit screen width
233+
lines, total_width = make_lines(current_phrase)
234+
235+
total_height = len(lines) * LINE_HEIGHT
236+
needs_scrolling = total_height > (MAX_VISIBLE_LINES * LINE_HEIGHT)
237+
238+
def update():
239+
global scroll_position, scroll_counter, is_fading, fade_step, fade_in, show_title_screen
240+
241+
# Handle title screen
242+
if show_title_screen:
243+
if any([thumby.buttonA.justPressed(), thumby.buttonB.justPressed(),
244+
thumby.buttonU.justPressed(), thumby.buttonD.justPressed()]):
245+
show_title_screen = False
246+
start_fade_transition()
247+
fade_in = True # Skip fade out, just fade in the first card
248+
return
249+
250+
# Handle fade transition
251+
if is_fading:
252+
fade_step += 1
253+
if fade_step >= FADE_STEPS:
254+
fade_step = 0
255+
if fade_in:
256+
# Fade in complete
257+
is_fading = False
258+
else:
259+
# Fade out complete, select new phrase and start fade in
260+
fade_in = True
261+
select_random_phrase()
262+
return
263+
264+
# Check for button presses
265+
if thumby.buttonA.justPressed() or thumby.buttonB.justPressed():
266+
start_fade_transition()
267+
return
268+
269+
if thumby.buttonU.pressed() and needs_scrolling and scroll_position > 0:
270+
scroll_position -= SCROLL_SPEED
271+
272+
if thumby.buttonD.pressed() and needs_scrolling and scroll_position < max(0, total_height - (MAX_VISIBLE_LINES * LINE_HEIGHT)):
273+
scroll_position += SCROLL_SPEED
274+
275+
# Auto-scroll if needed
276+
if needs_scrolling:
277+
scroll_counter += 1
278+
if scroll_counter >= SCROLL_DELAY:
279+
scroll_counter = 0
280+
scroll_position += SCROLL_SPEED
281+
282+
# Loop the scrolling
283+
if scroll_position >= total_height:
284+
scroll_position = -MAX_VISIBLE_LINES * LINE_HEIGHT
285+
286+
def draw():
287+
# Handle title screen
288+
if show_title_screen:
289+
draw_title_screen()
290+
return
291+
292+
thumby.display.fill(CARD_BG)
293+
294+
if is_fading:
295+
# Draw fading effect
296+
intensity = fade_step / FADE_STEPS
297+
if not fade_in:
298+
# Fading out (to black)
299+
fill_value = CARD_FG
300+
pattern_value = round(intensity * 100)
301+
else:
302+
# Fading in (from black)
303+
fill_value = CARD_FG
304+
pattern_value = round((1 - intensity) * 100)
305+
306+
thumby.display.fill(1 - fill_value) # Fill opposite of our fade color
307+
308+
# Create a dithering pattern for the fade effect
309+
for y in range(SCREEN_HEIGHT):
310+
for x in range(SCREEN_WIDTH):
311+
if (x + y) % 4 == 0 and random.randint(0, 100) < pattern_value:
312+
thumby.display.setPixel(x, y, fill_value)
313+
314+
else:
315+
vertical_offset = 0
316+
if not needs_scrolling and total_height < SCREEN_HEIGHT:
317+
vertical_offset = (SCREEN_HEIGHT - total_height) // 2
318+
319+
# Draw visible lines of text
320+
visible_start = scroll_position // LINE_HEIGHT
321+
visible_offset = scroll_position % LINE_HEIGHT
322+
323+
text_x = max(1, (SCREEN_WIDTH - total_width) // 2)
324+
thumby.display.setFont(BIG_FONT, BIG_FONT_WIDTH, BIG_FONT_HEIGHT, FONT_SPACING)
325+
for i in range(visible_start, min(len(lines), visible_start + MAX_VISIBLE_LINES + 1)):
326+
y = (i - visible_start) * LINE_HEIGHT - visible_offset + vertical_offset
327+
if 0 <= y < SCREEN_HEIGHT:
328+
thumby.display.drawText(lines[i], text_x, y, CARD_FG)
329+
330+
# Draw scrollbar if needed
331+
if needs_scrolling:
332+
scrollbar_height = max(3, (MAX_VISIBLE_LINES * LINE_HEIGHT) * MAX_VISIBLE_LINES * LINE_HEIGHT // total_height)
333+
scrollbar_position = min(SCREEN_HEIGHT - scrollbar_height - 1,
334+
(scroll_position * (SCREEN_HEIGHT - scrollbar_height - 2) //
335+
max(1, total_height - MAX_VISIBLE_LINES * LINE_HEIGHT)))
336+
337+
thumby.display.drawRectangle(SCREEN_WIDTH - 3, scrollbar_position, 2, scrollbar_height, CARD_FG)
338+
339+
thumby.display.update()
340+
341+
# Initialize with a random phrase
342+
select_random_phrase()
343+
344+
# Main app loop
345+
while True:
346+
update()
347+
draw()
348+
time.sleep(1/30) # Cap at 30 FPS
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A Thumby version of Brian Eno and Peter Schmidt's Oblique Strategies deck:
2+
https://en.wikipedia.org/wiki/Oblique_Strategies
3+
4+
Use it to shake up your thinking when you're stuck on a problem.
372 KB
Binary file not shown.

0 commit comments

Comments
 (0)