Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Hook your PyPortal Titano to your computer via a known good USB data+power cable. It should show up as a thumb drive named CIRCUITPY.
Using File Explorer/Finder (depending on your Operating System), drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
Once the game restarts, it will connect to WiFi and start using OpenAI to generate the game text. When presented with options, just touch the screen in one of the 4 quadrants to make your choice. To restart the game, just use the reset button to start fresh.
Head on to the next page for explanation of key parts of the code and hints on how to modify it to create your own adventure.

# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries # SPDX-License-Identifier: MIT import os import traceback import adafruit_connection_manager import adafruit_requests import adafruit_touchscreen from adafruit_ticks import ticks_ms, ticks_add, ticks_less from adafruit_bitmap_font.bitmap_font import load_font from adafruit_display_text.bitmap_label import Label from adafruit_display_text import wrap_text_to_pixels import board import displayio import supervisor import terminalio from adafruit_esp32spi import adafruit_esp32spi from digitalio import DigitalInOut # Use # https://github.com/adafruit/Adafruit_CircuitPython_Touchscreen/blob/main/examples/touchscreen_calibrator_built_in.py # to calibrate your touchscreen touchscreen_calibration=((6616, 60374), (8537, 57269)) # set to True to use openapi, False for quicker testing of the rest of the game # logic use_openai = True # Place the key in your settings.toml file openai_api_key = os.getenv("OPENAI_API_KEY") # Select a 14-point font for the PyPortal titano, 10-point for original & Pynt if board.DISPLAY.width > 320: nice_font = load_font("helvR14.pcf") else: nice_font = load_font("helvR10.pcf") line_spacing = 0.75 # Customize this prompt as you see fit to create a different experience base_prompt = """ You are an AI helping the player play an endless text adventure game. You will stay in character as the GM. The goal of the game is to save the Zorque mansion from being demolished. The \ game starts outside the abandoned Zorque mansion. As GM, never let the player die; they always survive a situation, no matter how \ harrowing. At each step: * Offer a short description of my surroundings (1 paragraph) * List the items I am carrying, if any * Offer me 4 terse numbered action choices (1 or 2 words each) In any case, be relatively terse and keep word counts small. In case the player wins (or loses) start a fresh game. """ clear='\033[2J' # variable for setup requests.Session requests = None # pylint: disable=global-statement def set_up_wifi(): global requests print(end=clear) if openai_api_key is None: print( "please set OPENAPI_API_KEY in settings.toml" ) raise SystemExit wifi_ssid = os.getenv('WIFI_SSID') wifi_password = os.getenv('WIFI_PASSWORD') if wifi_ssid is None: print( "please set WIFI_SSID and WIFI_PASSWORD in settings.toml" ) raise SystemExit esp_cs = DigitalInOut(board.ESP_CS) esp_ready = DigitalInOut(board.ESP_BUSY) esp_reset = DigitalInOut(board.ESP_RESET) spi = board.SPI() esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp_cs, esp_ready, esp_reset) pool = adafruit_connection_manager.get_radio_socketpool(esp) ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) requests = adafruit_requests.Session(pool, ssl_context) while not esp.is_connected: print("Connecting to AP...") try: esp.connect_AP(wifi_ssid, wifi_password) except Exception as e: # pylint: disable=broad-except print("could not connect to AP, retrying: ", e) for ap in esp.scan_networks(): print("%-24s RSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"])) continue print("Connected to WiFi") def terminal_label(text, width_in_chars, palette, x, y): label = displayio.TileGrid(terminalio.FONT.bitmap, pixel_shader=palette, width=width_in_chars, height=1, tile_width=glyph_width, tile_height=glyph_height) label.x = x label.y = y term = terminalio.Terminal(label, terminalio.FONT) term.write(f"{text: ^{width_in_chars-1}}") return label def terminal_palette(fg=0xffffff, bg=0): p = displayio.Palette(2) p[0] = bg p[1] = fg return p class WrappedTextDisplay: def __init__(self): self.line_offset = 0 self.lines = [] def add_text(self, text): self.lines.extend(wrap_text_to_pixels(text, use_width, nice_font)) def set_text(self, text): self.lines = wrap_text_to_pixels(text, use_width, nice_font) self.line_offset = 0 def scroll_to_end(self): self.line_offset = self.max_offset() def scroll_next_line(self): max_offset = self.max_offset() if max_offset > 0: line_offset = self.line_offset + 1 self.line_offset = line_offset % (max_offset + 1) def max_offset(self): return max(0, len(self.lines) - max_lines) def on_last_line(self): return self.line_offset == self.max_offset() def refresh(self): text = '\n'.join(self.lines[self.line_offset : self.line_offset + max_lines]) terminal.text = text board.DISPLAY.refresh() wrapped_text_display = WrappedTextDisplay() def print_wrapped(text): wrapped_text_display.set_text(text) wrapped_text_display.refresh() def make_full_prompt(action): return session + [{"role": "user", "content": f"PLAYER: {action}"}] def record_game_step(action, response): session.extend([ {"role": "user", "content": f"PLAYER: {action}"}, {"role": "assistant", "content": response.strip()}, ]) # Keep a limited number of exchanges in the prompt del session[1:-5] def get_one_completion(full_prompt): if not use_openai: return f"""\ This is a canned response in offline mode. The player's last choice was as follows: {full_prompt[-1]['content']} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ incididunt ut labore et dolore magna aliqua. Nulla aliquet enim tortor at \ auctor urna. Arcu ac tortor dignissim convallis aenean et tortor at. Dapibus \ ultrices in iaculis nunc sed augue. Enim nec dui nunc mattis enim ut tellus \ elementum sagittis. Sit amet mattis vulputate enim nulla. Ultrices in iaculis \ nunc sed augue lacus. Pulvinar neque laoreet suspendisse interdum consectetur \ libero id faucibus nisl. Aenean pharetra magna ac placerat vestibulum lectus \ mauris ultrices eros. Imperdiet nulla malesuada pellentesque elit eget. Tellus \ at urna condimentum mattis pellentesque id nibh tortor. Velit dignissim sodales \ ut eu sem integer vitae. Id ornare arcu odio ut sem nulla pharetra diam sit. 1: Stand in the place where you live 2: Now face West 3: Think about the place where you live 4: Wonder why you haven't before """.strip() try: response = requests.post( "https://api.openai.com/v1/chat/completions", json={"model": "gpt-3.5-turbo", "messages": full_prompt}, headers={ "Authorization": f"Bearer {openai_api_key}", }, ) except Exception as e: # pylint: disable=broad-except print("requests exception", e) return None if response.status_code != 200: print("requests status", response.status_code) return None j = response.json() result = j["choices"][0]["message"]["content"] return result.strip() def get_touchscreen_choice(): # Wait for screen to be released while ts.touch_point: pass # Wait for screen to be pressed touch_count = 0 deadline = ticks_add(ticks_ms(), 1000) while True: t = ts.touch_point if t is not None: touch_count += 1 if touch_count > 5: break else: touch_count = 0 if wrapped_text_display.max_offset() > 0 and ticks_less(deadline, ticks_ms()): wrapped_text_display.scroll_next_line() wrapped_text_display.refresh() deadline = ticks_add(deadline, 5000 if wrapped_text_display.on_last_line() else 1000) # Depending on the quadrant of the screen, make a choice x, y, _ = t result = 1 if x > board.DISPLAY.width / 2: result = result + 1 if y > board.DISPLAY.height / 2: result = result + 2 return result def run_game_step(forced_choice=None): if forced_choice: choice = forced_choice else: choice = get_touchscreen_choice() wrapped_text_display.add_text(f"\nPLAYER: {choice}") wrapped_text_display.scroll_to_end() wrapped_text_display.refresh() prompt = make_full_prompt(choice) for _ in range(3): result = get_one_completion(prompt) if result is not None: break else: raise ValueError("Error getting completion from OpenAI") print(result) wrapped_text_display.set_text(result) wrapped_text_display.refresh() record_game_step(choice, result) if use_openai: # Only set up wifi if using openai set_up_wifi() # Set up the touchscreen # These pins are used as both analog and digital! XL, XR and YU must be analog # and digital capable. YD just need to be digital ts = adafruit_touchscreen.Touchscreen( board.TOUCH_XL, board.TOUCH_XR, board.TOUCH_YD, board.TOUCH_YU, calibration=touchscreen_calibration, size=(board.DISPLAY.width, board.DISPLAY.height) ) # Set up the 4 onscreen buttons & embedded terminal main_group = displayio.Group() main_group.x = 4 main_group.y = 4 # Determine the size of everything glyph_width, glyph_height = terminalio.FONT.get_bounding_box() use_height = board.DISPLAY.height - 4 use_width = board.DISPLAY.width - 4 # Game text is displayed on this wdget terminal = Label( font=nice_font, color=0xFFFFFF, background_color=0, line_spacing=line_spacing, anchor_point=(0, 0), anchored_position=(0, glyph_height + 1), ) max_lines = (use_height - 2 * glyph_height) // int( nice_font.get_bounding_box()[1] * terminal.line_spacing ) main_group.append(terminal) # Indicate what each quadrant of the screen does when tapped label_width = use_width // (glyph_width * 2) main_group.append(terminal_label('1', label_width, terminal_palette(0, 0xffff00), 0, 0)) main_group.append(terminal_label('2', label_width, terminal_palette(0, 0x00ffff), use_width - label_width*glyph_width, 0)) main_group.append(terminal_label('3', label_width, terminal_palette(0, 0xff00ff), 0, use_height-glyph_height)) main_group.append(terminal_label('4', label_width, terminal_palette(0, 0x00ff00), use_width - label_width*glyph_width, use_height-glyph_height)) # Show our stuff on the screen board.DISPLAY.auto_refresh = False board.DISPLAY.root_group = main_group board.DISPLAY.refresh() # Track the game so far. ALways start with the base prompt. session = [ {"role": "system", "content": base_prompt.strip()}, ] try: run_game_step("New game") while True: run_game_step() except Exception as e: # pylint: disable=broad-except traceback.print_exception(e) # pylint: disable=no-value-for-parameter print_wrapped("An error occurred (more details on REPL).\nTouch the screen to re-load") board.DISPLAY.refresh() get_touchscreen_choice() supervisor.reload()
Page last edited January 22, 2025
Text editor powered by tinymce.