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
0 commit comments