Skip to content

Commit f96841c

Browse files
committed
code for new guide
1 parent 0ba59ff commit f96841c

File tree

1 file changed

+258
-0
lines changed
  • CircuitPython_Zorque_Text_Game_openai

1 file changed

+258
-0
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
import os
4+
5+
import adafruit_esp32spi.adafruit_esp32spi_socket as socket
6+
import adafruit_requests as requests
7+
import adafruit_touchscreen
8+
import board
9+
import displayio
10+
import terminalio
11+
from adafruit_esp32spi import adafruit_esp32spi
12+
from digitalio import DigitalInOut
13+
14+
# Use
15+
# https://github.com/adafruit/Adafruit_CircuitPython_Touchscreen/blob/main/examples/touchscreen_calibrator_built_in.py
16+
# to calibrate your touchscreen
17+
touchscreen_calibration=((6616, 60374), (8537, 57269))
18+
19+
20+
# set to True to use openapi, False for quicker testing of the rest of the game
21+
# logic
22+
use_openai = True
23+
24+
# Place the key in your settings.toml file
25+
openai_api_key = os.getenv("OPENAI_API_KEY")
26+
27+
# Customize this prompt as you see fit to create a different experience
28+
base_prompt = """
29+
You are an AI helping the player play an endless text adventure game. You will stay in character as the GM.
30+
31+
The goal of the game is to save the Zorque mansion from being demolished. The
32+
game starts outside the abandonded Zorque mansion.
33+
34+
As GM, never let the player die; they always survive a situation, no matter how
35+
harrowing.
36+
At each step:
37+
* Offer a short description of my surroundings (1 paragraph)
38+
* List the items I am carrying, if any
39+
* Offer me 4 terse numbered action choices (1 or 2 words each)
40+
41+
In any case, be relatively terse and keep word counts small.
42+
43+
In case the player wins (or loses) start a fresh game.
44+
"""
45+
46+
clear='\033[2J'
47+
48+
def set_up_wifi():
49+
print(end=clear)
50+
if openai_api_key is None:
51+
print(
52+
"please set OPENAPI_API_KEY in settings.toml"
53+
)
54+
raise SystemExit
55+
56+
wifi_ssid = os.getenv('WIFI_SSID')
57+
wifi_password = os.getenv('WIFI_PASSWORD')
58+
if wifi_ssid is None:
59+
print(
60+
"please set WIFI_SSID and WIFI_PASSWORD in settings.toml"
61+
)
62+
raise SystemExit
63+
64+
esp_cs = DigitalInOut(board.ESP_CS)
65+
esp_ready = DigitalInOut(board.ESP_BUSY)
66+
esp_reset = DigitalInOut(board.ESP_RESET)
67+
68+
spi = board.SPI()
69+
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp_cs, esp_ready, esp_reset)
70+
requests.set_socket(socket, esp)
71+
72+
while not esp.is_connected:
73+
print("Connecting to AP...")
74+
try:
75+
esp.connect_AP(wifi_ssid, wifi_password)
76+
except Exception as e: # pylint: disable=broad-except
77+
print("could not connect to AP, retrying: ", e)
78+
for ap in esp.scan_networks():
79+
print("%-24s RSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"]))
80+
continue
81+
print("Connected to WiFi")
82+
83+
def terminal_label(text, width_in_chars, palette, x, y):
84+
label = displayio.TileGrid(terminalio.FONT.bitmap, pixel_shader=palette,
85+
width=width_in_chars, height=1, tile_width=glyph_width,
86+
tile_height=glyph_height)
87+
label.x = x
88+
label.y = y
89+
term = terminalio.Terminal(label, terminalio.FONT)
90+
term.write(f"{text: ^{width_in_chars-1}}")
91+
return label
92+
93+
def terminal_palette(fg=0xffffff, bg=0):
94+
p = displayio.Palette(2)
95+
p[0] = bg
96+
p[1] = fg
97+
return p
98+
99+
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()
118+
119+
def make_full_prompt(action):
120+
return session + [{"role": "user", "content": f"PLAYER: {action}"}]
121+
122+
def record_game_step(action, response):
123+
session.extend([
124+
{"role": "user", "content": f"PLAYER: {action}"},
125+
{"role": "assistant", "content": response.strip()},
126+
])
127+
# Keep a limited number of exchanges in the prompt
128+
del session[1:-5]
129+
130+
def get_one_completion(full_prompt):
131+
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()
134+
try:
135+
response = requests.post(
136+
"https://api.openai.com/v1/chat/completions",
137+
json={"model": "gpt-3.5-turbo", "messages": full_prompt},
138+
headers={
139+
"Authorization": f"Bearer {openai_api_key}",
140+
},
141+
)
142+
except Exception as e: # pylint: disable=broad-except
143+
print("requests exception", e)
144+
return None
145+
if response.status_code != 200:
146+
print("requests status", response.status_code)
147+
return None
148+
j = response.json()
149+
result = j["choices"][0]["message"]["content"]
150+
return result.strip()
151+
152+
def get_touchscreen_choice():
153+
# Wait for screen to be released
154+
while ts.touch_point:
155+
pass
156+
157+
# Wait for screen to be pressed
158+
touch_count = 0
159+
while True:
160+
t = ts.touch_point
161+
if t is not None:
162+
touch_count += 1
163+
if touch_count > 5:
164+
break
165+
else:
166+
touch_count = 0
167+
168+
# Depending on the quadrant of the screen, make a choice
169+
x, y, _ = t
170+
result = 1
171+
if x > board.DISPLAY.width / 2:
172+
result = result + 1
173+
if y > board.DISPLAY.height / 2:
174+
result = result + 2
175+
return result
176+
177+
def run_game_step(forced_choice=None):
178+
if forced_choice:
179+
choice = forced_choice
180+
else:
181+
choice = get_touchscreen_choice()
182+
print_wrapped(f"\n\nPLAYER: {choice}")
183+
prompt = make_full_prompt(choice)
184+
for _ in range(3):
185+
result = get_one_completion(prompt)
186+
if result is not None:
187+
break
188+
else:
189+
raise ValueError("Error getting completion from OpenAI")
190+
print(result)
191+
terminal.write(clear)
192+
print_wrapped(result)
193+
194+
record_game_step(choice, result)
195+
196+
if use_openai:
197+
# Only set up wifi if using openai
198+
set_up_wifi()
199+
200+
# Set up the touchscreen
201+
# These pins are used as both analog and digital! XL, XR and YU must be analog
202+
# and digital capable. YD just need to be digital
203+
ts = adafruit_touchscreen.Touchscreen(
204+
board.TOUCH_XL,
205+
board.TOUCH_XR,
206+
board.TOUCH_YD,
207+
board.TOUCH_YU,
208+
calibration=touchscreen_calibration,
209+
size=(board.DISPLAY.width, board.DISPLAY.height)
210+
)
211+
212+
# Set up the 4 onscreen buttons & embedded terminal
213+
main_group = displayio.Group()
214+
main_group.x = 4
215+
main_group.y = 4
216+
217+
# Determine the size of everything
218+
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
223+
224+
# 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)
232+
233+
# Indicate what each quadrant of the screen does when tapped
234+
label_width = use_width // (glyph_width * 2)
235+
main_group.append(terminal_label('1', label_width, terminal_palette(0, 0xffff00), 0, 0))
236+
main_group.append(terminal_label('2', label_width, terminal_palette(0, 0x00ffff),
237+
use_width - label_width*glyph_width, 0))
238+
main_group.append(terminal_label('3', label_width, terminal_palette(0, 0xff00ff),
239+
0, use_height-2*glyph_height))
240+
main_group.append(terminal_label('4', label_width, terminal_palette(0, 0x00ff00),
241+
use_width - label_width*glyph_width, use_height-2*glyph_height))
242+
243+
# Show our stuff on the screen
244+
board.DISPLAY.auto_refresh = False
245+
board.DISPLAY.root_group = main_group
246+
board.DISPLAY.refresh()
247+
248+
# Track the game so far. ALways start with the base prompt.
249+
session = [
250+
{"role": "system", "content": base_prompt.strip()},
251+
]
252+
253+
try:
254+
run_game_step("New game")
255+
while True:
256+
run_game_step()
257+
except (EOFError, KeyboardInterrupt) as e:
258+
raise SystemExit from e

0 commit comments

Comments
 (0)