Skip to content

Commit 0180bb4

Browse files
authored
Merge pull request #2436 from jepler/zorque
update the Zorque game
2 parents b7275d4 + e804e29 commit 0180bb4

File tree

3 files changed

+109
-41
lines changed

3 files changed

+109
-41
lines changed

CircuitPython_Zorque_Text_Game_openai/code.py

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
22
# SPDX-License-Identifier: MIT
33
import os
4+
import traceback
45

56
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
67
import adafruit_requests as requests
78
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
813
import board
914
import displayio
15+
import supervisor
1016
import terminalio
1117
from adafruit_esp32spi import adafruit_esp32spi
1218
from digitalio import DigitalInOut
@@ -24,15 +30,23 @@
2430
# Place the key in your settings.toml file
2531
openai_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
2841
base_prompt = """
2942
You 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+
3650
At 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+
99149
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()
118152

119153
def make_full_prompt(action):
120154
return session + [{"role": "user", "content": f"PLAYER: {action}"}]
@@ -129,8 +163,26 @@ def record_game_step(action, response):
129163

130164
def 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\nPLAYER: {choice}")
240+
wrapped_text_display.add_text(f"\nPLAYER: {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
218279
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
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
234298
label_width = use_width // (glyph_width * 2)
235299
main_group.append(terminal_label('1', label_width, terminal_palette(0, 0xffff00), 0, 0))
236300
main_group.append(terminal_label('2', label_width, terminal_palette(0, 0x00ffff),
237301
use_width - label_width*glyph_width, 0))
238302
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))
240304
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))
242306

243307
# Show our stuff on the screen
244308
board.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).\nTouch the screen to re-load")
324+
board.DISPLAY.refresh()
325+
get_touchscreen_choice()
326+
supervisor.reload()
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)