1
1
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
2
2
# SPDX-License-Identifier: MIT
3
3
import os
4
+ import traceback
4
5
5
6
import adafruit_esp32spi .adafruit_esp32spi_socket as socket
6
7
import adafruit_requests as requests
7
8
import 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
8
13
import board
9
14
import displayio
15
+ import supervisor
10
16
import terminalio
11
17
from adafruit_esp32spi import adafruit_esp32spi
12
18
from digitalio import DigitalInOut
24
30
# Place the key in your settings.toml file
25
31
openai_api_key = os .getenv ("OPENAI_API_KEY" )
26
32
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
+
27
40
# Customize this prompt as you see fit to create a different experience
28
41
base_prompt = """
29
42
You are an AI helping the player play an endless text adventure game. You will stay in character as the GM.
30
43
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.
33
46
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 \
35
48
harrowing.
49
+
36
50
At each step:
37
51
* Offer a short description of my surroundings (1 paragraph)
38
52
* List the items I am carrying, if any
@@ -96,25 +110,45 @@ def terminal_palette(fg=0xffffff, bg=0):
96
110
p [1 ] = fg
97
111
return p
98
112
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
+
99
149
def 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 ()
118
152
119
153
def make_full_prompt (action ):
120
154
return session + [{"role" : "user" , "content" : f"PLAYER: { action } " }]
@@ -129,8 +163,26 @@ def record_game_step(action, response):
129
163
130
164
def get_one_completion (full_prompt ):
131
165
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 ()
134
186
try :
135
187
response = requests .post (
136
188
"https://api.openai.com/v1/chat/completions" ,
@@ -156,6 +208,7 @@ def get_touchscreen_choice():
156
208
157
209
# Wait for screen to be pressed
158
210
touch_count = 0
211
+ deadline = ticks_add (ticks_ms (), 1000 )
159
212
while True :
160
213
t = ts .touch_point
161
214
if t is not None :
@@ -164,6 +217,11 @@ def get_touchscreen_choice():
164
217
break
165
218
else :
166
219
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 )
167
225
168
226
# Depending on the quadrant of the screen, make a choice
169
227
x , y , _ = t
@@ -179,7 +237,10 @@ def run_game_step(forced_choice=None):
179
237
choice = forced_choice
180
238
else :
181
239
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
+
183
244
prompt = make_full_prompt (choice )
184
245
for _ in range (3 ):
185
246
result = get_one_completion (prompt )
@@ -188,8 +249,8 @@ def run_game_step(forced_choice=None):
188
249
else :
189
250
raise ValueError ("Error getting completion from OpenAI" )
190
251
print (result )
191
- terminal . write ( clear )
192
- print_wrapped ( result )
252
+ wrapped_text_display . set_text ( result )
253
+ wrapped_text_display . refresh ( )
193
254
194
255
record_game_step (choice , result )
195
256
@@ -216,29 +277,32 @@ def run_game_step(forced_choice=None):
216
277
217
278
# Determine the size of everything
218
279
glyph_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
223
282
224
283
# 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 )
232
296
233
297
# Indicate what each quadrant of the screen does when tapped
234
298
label_width = use_width // (glyph_width * 2 )
235
299
main_group .append (terminal_label ('1' , label_width , terminal_palette (0 , 0xffff00 ), 0 , 0 ))
236
300
main_group .append (terminal_label ('2' , label_width , terminal_palette (0 , 0x00ffff ),
237
301
use_width - label_width * glyph_width , 0 ))
238
302
main_group .append (terminal_label ('3' , label_width , terminal_palette (0 , 0xff00ff ),
239
- 0 , use_height - 2 * glyph_height ))
303
+ 0 , use_height - glyph_height ))
240
304
main_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 ))
242
306
243
307
# Show our stuff on the screen
244
308
board .DISPLAY .auto_refresh = False
@@ -254,5 +318,9 @@ def run_game_step(forced_choice=None):
254
318
run_game_step ("New game" )
255
319
while True :
256
320
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