11# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
22# SPDX-License-Identifier: MIT
33import os
4+ import traceback
45
56import adafruit_esp32spi .adafruit_esp32spi_socket as socket
67import adafruit_requests as requests
78import adafruit_touchscreen
9+ from adafruit_ticks import ticks_ms , ticks_add , ticks_less
10+ from adafruit_bitmap_font .bitmap_font import load_font
11+ from adafruit_display_text .bitmap_label import Label
12+ from adafruit_display_text import wrap_text_to_pixels
813import board
914import displayio
15+ import supervisor
1016import terminalio
1117from adafruit_esp32spi import adafruit_esp32spi
1218from digitalio import DigitalInOut
2430# Place the key in your settings.toml file
2531openai_api_key = os .getenv ("OPENAI_API_KEY" )
2632
33+ # Select a 14-point font for the PyPortal titano, 10-point for original & Pynt
34+ if board .DISPLAY .width > 320 :
35+ nice_font = load_font ("helvR14.pcf" )
36+ else :
37+ nice_font = load_font ("helvR10.pcf" )
38+ line_spacing = 0.75
39+
2740# Customize this prompt as you see fit to create a different experience
2841base_prompt = """
2942You are an AI helping the player play an endless text adventure game. You will stay in character as the GM.
3043
31- The goal of the game is to save the Zorque mansion from being demolished. The
32- game starts outside the abandonded Zorque mansion.
44+ The goal of the game is to save the Zorque mansion from being demolished. The \
45+ game starts outside the abandoned Zorque mansion.
3346
34- As GM, never let the player die; they always survive a situation, no matter how
47+ As GM, never let the player die; they always survive a situation, no matter how \
3548 harrowing.
49+
3650At each step:
3751 * Offer a short description of my surroundings (1 paragraph)
3852 * List the items I am carrying, if any
@@ -96,25 +110,45 @@ def terminal_palette(fg=0xffffff, bg=0):
96110 p [1 ] = fg
97111 return p
98112
113+ class WrappedTextDisplay :
114+ def __init__ (self ):
115+ self .line_offset = 0
116+ self .lines = []
117+
118+ def add_text (self , text ):
119+ self .lines .extend (wrap_text_to_pixels (text , use_width , nice_font ))
120+
121+ def set_text (self , text ):
122+ self .lines = wrap_text_to_pixels (text , use_width , nice_font )
123+ self .line_offset = 0
124+
125+ def scroll_to_end (self ):
126+ self .line_offset = self .max_offset ()
127+
128+ def scroll_next_line (self ):
129+ max_offset = self .max_offset ()
130+ if max_offset > 0 :
131+ line_offset = self .line_offset + 1
132+ self .line_offset = line_offset % (max_offset + 1 )
133+
134+ def max_offset (self ):
135+ return max (0 , len (self .lines ) - max_lines )
136+
137+ def on_last_line (self ):
138+ return self .line_offset == self .max_offset ()
139+
140+ def refresh (self ):
141+ text = '\n ' .join (self .lines [self .line_offset : self .line_offset + max_lines ])
142+ # Work around https://github.com/adafruit/Adafruit_CircuitPython_Display_Text/issues/183
143+ while '\n \n ' in text :
144+ text = text .replace ('\n \n ' , '\n \n ' )
145+ terminal .text = text
146+ board .DISPLAY .refresh ()
147+ wrapped_text_display = WrappedTextDisplay ()
148+
99149def print_wrapped (text ):
100- print (text )
101- maxwidth = main_text .width
102- for line in text .split ("\n " ):
103- col = 0
104- sp = ''
105- for word in line .split ():
106- newcol = col + len (sp ) + len (word )
107- if newcol < maxwidth :
108- terminal .write (sp + word )
109- col = newcol
110- else :
111- terminal .write ('\r \n ' )
112- terminal .write (word )
113- col = len (word )
114- sp = ' '
115- if sp or not line :
116- terminal .write ('\r \n ' )
117- board .DISPLAY .refresh ()
150+ wrapped_text_display .set_text (text )
151+ wrapped_text_display .refresh ()
118152
119153def make_full_prompt (action ):
120154 return session + [{"role" : "user" , "content" : f"PLAYER: { action } " }]
@@ -129,8 +163,26 @@ def record_game_step(action, response):
129163
130164def get_one_completion (full_prompt ):
131165 if not use_openai :
132- return f"""This is a canned response in offline mode. The player's last
133- choice was as follows: { full_prompt [- 1 ]['content' ]} """ .strip ()
166+ return f"""\
167+ This is a canned response in offline mode. The player's last choice was as follows:
168+ { full_prompt [- 1 ]['content' ]}
169+
170+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \
171+ incididunt ut labore et dolore magna aliqua. Nulla aliquet enim tortor at \
172+ auctor urna. Arcu ac tortor dignissim convallis aenean et tortor at. Dapibus \
173+ ultrices in iaculis nunc sed augue. Enim nec dui nunc mattis enim ut tellus \
174+ elementum sagittis. Sit amet mattis vulputate enim nulla. Ultrices in iaculis \
175+ nunc sed augue lacus. Pulvinar neque laoreet suspendisse interdum consectetur \
176+ libero id faucibus nisl. Aenean pharetra magna ac placerat vestibulum lectus \
177+ mauris ultrices eros. Imperdiet nulla malesuada pellentesque elit eget. Tellus \
178+ at urna condimentum mattis pellentesque id nibh tortor. Velit dignissim sodales \
179+ ut eu sem integer vitae. Id ornare arcu odio ut sem nulla pharetra diam sit.
180+
181+ 1: Stand in the place where you live
182+ 2: Now face West
183+ 3: Think about the place where you live
184+ 4: Wonder why you haven't before
185+ """ .strip ()
134186 try :
135187 response = requests .post (
136188 "https://api.openai.com/v1/chat/completions" ,
@@ -156,6 +208,7 @@ def get_touchscreen_choice():
156208
157209 # Wait for screen to be pressed
158210 touch_count = 0
211+ deadline = ticks_add (ticks_ms (), 1000 )
159212 while True :
160213 t = ts .touch_point
161214 if t is not None :
@@ -164,6 +217,11 @@ def get_touchscreen_choice():
164217 break
165218 else :
166219 touch_count = 0
220+ if wrapped_text_display .max_offset () > 0 and ticks_less (deadline , ticks_ms ()):
221+ wrapped_text_display .scroll_next_line ()
222+ wrapped_text_display .refresh ()
223+ deadline = ticks_add (deadline ,
224+ 5000 if wrapped_text_display .on_last_line () else 1000 )
167225
168226 # Depending on the quadrant of the screen, make a choice
169227 x , y , _ = t
@@ -179,7 +237,10 @@ def run_game_step(forced_choice=None):
179237 choice = forced_choice
180238 else :
181239 choice = get_touchscreen_choice ()
182- print_wrapped (f"\n \n PLAYER: { choice } " )
240+ wrapped_text_display .add_text (f"\n PLAYER: { choice } " )
241+ wrapped_text_display .scroll_to_end ()
242+ wrapped_text_display .refresh ()
243+
183244 prompt = make_full_prompt (choice )
184245 for _ in range (3 ):
185246 result = get_one_completion (prompt )
@@ -188,8 +249,8 @@ def run_game_step(forced_choice=None):
188249 else :
189250 raise ValueError ("Error getting completion from OpenAI" )
190251 print (result )
191- terminal . write ( clear )
192- print_wrapped ( result )
252+ wrapped_text_display . set_text ( result )
253+ wrapped_text_display . refresh ( )
193254
194255 record_game_step (choice , result )
195256
@@ -216,29 +277,32 @@ def run_game_step(forced_choice=None):
216277
217278# Determine the size of everything
218279glyph_width , glyph_height = terminalio .FONT .get_bounding_box ()
219- use_height = board .DISPLAY .height - 8
220- use_width = board .DISPLAY .width - 8
221- terminal_width = use_width // glyph_width
222- terminal_height = use_height // glyph_height - 4
280+ use_height = board .DISPLAY .height - 4
281+ use_width = board .DISPLAY .width - 4
223282
224283# Game text is displayed on this wdget
225- main_text = displayio .TileGrid (terminalio .FONT .bitmap , pixel_shader = terminal_palette (),
226- width = terminal_width , height = terminal_height , tile_width = glyph_width ,
227- tile_height = glyph_height )
228- main_text .x = 4
229- main_text .y = 4 + glyph_height
230- terminal = terminalio .Terminal (main_text , terminalio .FONT )
231- main_group .append (main_text )
284+ terminal = Label (
285+ font = nice_font ,
286+ color = 0xFFFFFF ,
287+ background_color = 0 ,
288+ line_spacing = line_spacing ,
289+ anchor_point = (0 , 0 ),
290+ anchored_position = (0 , glyph_height + 1 ),
291+ )
292+ max_lines = (use_height - 2 * glyph_height ) // int (
293+ nice_font .get_bounding_box ()[1 ] * terminal .line_spacing
294+ )
295+ main_group .append (terminal )
232296
233297# Indicate what each quadrant of the screen does when tapped
234298label_width = use_width // (glyph_width * 2 )
235299main_group .append (terminal_label ('1' , label_width , terminal_palette (0 , 0xffff00 ), 0 , 0 ))
236300main_group .append (terminal_label ('2' , label_width , terminal_palette (0 , 0x00ffff ),
237301 use_width - label_width * glyph_width , 0 ))
238302main_group .append (terminal_label ('3' , label_width , terminal_palette (0 , 0xff00ff ),
239- 0 , use_height - 2 * glyph_height ))
303+ 0 , use_height - glyph_height ))
240304main_group .append (terminal_label ('4' , label_width , terminal_palette (0 , 0x00ff00 ),
241- use_width - label_width * glyph_width , use_height - 2 * glyph_height ))
305+ use_width - label_width * glyph_width , use_height - glyph_height ))
242306
243307# Show our stuff on the screen
244308board .DISPLAY .auto_refresh = False
@@ -254,5 +318,9 @@ def run_game_step(forced_choice=None):
254318 run_game_step ("New game" )
255319 while True :
256320 run_game_step ()
257- except (EOFError , KeyboardInterrupt ) as e :
258- raise SystemExit from e
321+ except Exception as e : # pylint: disable=broad-except
322+ traceback .print_exception (e ) # pylint: disable=no-value-for-parameter
323+ print_wrapped ("An error occurred (more details on REPL).\n Touch the screen to re-load" )
324+ board .DISPLAY .refresh ()
325+ get_touchscreen_choice ()
326+ supervisor .reload ()
0 commit comments