# Infinite Text Adventure

## Overview

![](https://cdn-learn.adafruit.com/assets/assets/000/119/364/medium800thumb/circuitpython_ezgif.com-optimize%281%29.jpg?1678461817)

In this guide, you will learn how to use OpenAI's "chat" API to generate an endless text adventure game on the PyPortal family of devices; the PyPortal Titano is recommended for its larger screen.

By providing a suitable prompt to OpenAI, it will offer a fictional adventure where the next choice can be made on the PyPortal touchscreen. This choice, along with some of the previous text, is fed back to the API to get the result of the action. Because of the random factor in the text ChatGPT generates, and the branching choices, it's unlikely that two games would ever be the same.

Ultimately the "adventure" is not pre-planned and frequently makes unjustifiable creative leaps, but the author has found it fun to play with anyway.

If the "Zorque Mansion" scenario doesn't interest you, create a scenario of your choice by writing a sentence or two in natural human language describing it; no complicated coding is needed to tell a sci-fi story instead, or even to play in French instead of English!

Photos in this guide show the PyPortal Titano with the 3D printed "retro case". [Check out the full guide for that 3D printed project](https://learn.adafruit.com/pyportal-retro-compys).

## Parts
### Adafruit PyPortal Titano

[Adafruit PyPortal Titano](https://www.adafruit.com/product/4444)
The **PyPortal Titano** is the big sister to our [popular PyPortal](https://www.adafruit.com/product/4116) now with _twice as many pixels!_ The PyPortal is our easy-to-use IoT device that allows you to create all the things for the “Internet of...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4444)
[Related Guides to the Product](https://learn.adafruit.com/products/4444/guides)
![Hand holding PyPortal Titano development board with SAMD51, ESP32 Wifi, and 3.5" touchscreen TFT display.](https://cdn-shop.adafruit.com/640x480/4444-10.jpg)

### Pink and Purple Woven USB A to USB C Cable - 1 meter long

[Pink and Purple Woven USB A to USB C Cable - 1 meter long](https://www.adafruit.com/product/5153)
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also made for USB C for our modernized breakout boards, Feathers, and more.&nbsp;&nbsp;[If you want something just like it but for Micro B, we...](https://www.adafruit.com/product/4111)

Out of Stock
[Buy Now](https://www.adafruit.com/product/5153)
[Related Guides to the Product](https://learn.adafruit.com/products/5153/guides)
![Angled shot of coiled pink and purple USB cable with USB A and USB C connectors.](https://cdn-shop.adafruit.com/640x480/5153-02.jpg)

### Adafruit PyPortal - CircuitPython Powered Internet Display

[Adafruit PyPortal - CircuitPython Powered Internet Display](https://www.adafruit.com/product/4116)
 **PyPortal** , our easy-to-use IoT device that allows you to create all the things for the “Internet of Things” in minutes. Make custom touch screen interface GUIs, all open-source, and Python-powered using&nbsp;tinyJSON / APIs to get news, stock, weather, cat photos,...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4116)
[Related Guides to the Product](https://learn.adafruit.com/products/4116/guides)
![Front view of a Adafruit PyPortal - CircuitPython Powered Internet Display with a pyportal logo image on the display. ](https://cdn-shop.adafruit.com/640x480/4116-00.jpeg)

### Fully Reversible Pink/Purple USB A to micro B Cable - 1m long

[Fully Reversible Pink/Purple USB A to micro B Cable - 1m long](https://www.adafruit.com/product/4111)
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also fully reversible! That's right, you will save _seconds_ a day by not having to flip the cable around.

First let's talk about the cover and over-molding. We got these...

In Stock
[Buy Now](https://www.adafruit.com/product/4111)
[Related Guides to the Product](https://learn.adafruit.com/products/4111/guides)
![Fully Reversible Pink/Purple USB A to micro B Cable](https://cdn-shop.adafruit.com/640x480/4111-02.jpg)

### Part: OpenAI Account &amp; API Key
quantity: 1
A $2.00 budget suffices for multiple hours of play.
[OpenAI Account &amp; API Key](https://platform.openai.com/)

# Infinite Text Adventure

## CircuitPython

[CircuitPython](https://github.com/adafruit/circuitpython) is a derivative of [MicroPython](https://micropython.org) designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the **CIRCUITPY** &nbsp;"flash" drive to iterate.

The following instructions will show you how to install CircuitPython. If you've already installed CircuitPython but are looking to update it or reinstall it, the same steps work for that as well!

## Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

[Download the latest version of CircuitPython for this board via CircuitPython.org](https://circuitpython.org/board/pyportal_titano/)
 **Click the link above to download the latest version of CircuitPython for the PyPortal Titano.**

Download and save it to your desktop (or wherever is handy).

![adafruit_products_Titano_download_uf2.png](https://cdn-learn.adafruit.com/assets/assets/000/086/200/medium640/adafruit_products_Titano_download_uf2.png?1576865361)

Plug your PyPortal into your computer using a known-good USB cable.

**A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.**

Double-click the **Reset** button on the top in the middle (magenta arrow) on your board, and you will see the NeoPixel RGB LED (green arrow) turn green. If it turns red, check the USB cable, try another USB port, etc.&nbsp; **Note:** The little red LED next to the USB connector will pulse red. That's ok!

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

![adafruit_products_Titano_NeoPixel_reset_button.png](https://cdn-learn.adafruit.com/assets/assets/000/086/199/medium640/adafruit_products_Titano_NeoPixel_reset_button.png?1576865337)

You will see a new disk drive appear called **PORTALBOOT**.

Drag the **adafruit-circuitpython-pyportal-etc.uf2** file to **PORTALBOOT.**

![adafruit_products_Titano_PORTALBOOT.png](https://cdn-learn.adafruit.com/assets/assets/000/086/202/medium640/adafruit_products_Titano_PORTALBOOT.png?1576865564)

![adafruit_products_Titano_drag_UF2.png](https://cdn-learn.adafruit.com/assets/assets/000/086/204/medium640/adafruit_products_Titano_drag_UF2.png?1576865593)

The LED will flash. Then, the **PORTALBOOT** drive will disappear and a new disk drive called **CIRCUITPY** will appear.

If you haven't added any code to your board, the only file that will be present is **boot\_out.txt**. This is absolutely normal! It's time for you to add your **code.py** and get started!

That's it, you're done! :)

![adafruit_products_Titano_CIRCUITPY.png](https://cdn-learn.adafruit.com/assets/assets/000/086/206/medium640/adafruit_products_Titano_CIRCUITPY.png?1576865825)

## PyPortal Titano Default Files

Click below to download a zip of the files that shipped on the PyPortal Titano.

[PyPortal Titano Default Files](https://github.com/adafruit/circuitpython-default-files/tree/main/boards/pyportal_titano/5.x)
# Infinite Text Adventure

## Create an account with OpenAI

Info: 

In your web browser, visit [https://platform.openai.com/](https://platform.openai.com/)

Click the "sign up" link. Then, you can use your e-mail to sign up, or an existing Google or Microsoft account.

OpenAI may require additional steps such as e-mail or phone verification before you can log in to your account.

![circuitpython_Screenshot_2023-03-08_08-43-40.png](https://cdn-learn.adafruit.com/assets/assets/000/119/295/medium640/circuitpython_Screenshot_2023-03-08_08-43-40.png?1678286661)

Once you have completed the verification process and logged in, you will next create an API key. Use the menu in the far upper right corner (probably labeled "Personal") and then select "View API Keys".

![circuitpython_ksnip_20230308-084810.png](https://cdn-learn.adafruit.com/assets/assets/000/119/296/medium640/circuitpython_ksnip_20230308-084810.png?1678286921)

Then, create a fresh API key by clicking "Create new secret key".

![circuitpython_ksnip_20230308-084917.png](https://cdn-learn.adafruit.com/assets/assets/000/119/298/medium640/circuitpython_ksnip_20230308-084917.png?1678287073)

Save this secret key in the file **settings.toml** on the **CIRCUITPY** drive in a line that looks like

```
OPENAI\_API\_KEY="sk-b6...kP5"
```

This file also requires your WiFI credentials, see the next page of the guide for the details.

![circuitpython_ksnip_20230308-084955.png](https://cdn-learn.adafruit.com/assets/assets/000/119/299/medium640/circuitpython_ksnip_20230308-084955.png?1678287111)

At the time of writing, OpenAI provides a free credit with new accounts. After the free credit is used or expires, you'll need to enter a credit card in your billing information to keep using the service.

Using the project tends to cost a few cents per session at most, and it's easy to limit your monthly bill to a pre-set amount such as $8.00.

To set a hard usage limit per month, visit the "Usage Limits" section of the OpenAI website.

![circuitpython_ksnip_20230310-085720.png](https://cdn-learn.adafruit.com/assets/assets/000/119/338/medium640/circuitpython_ksnip_20230310-085720.png?1678460269)

![circuitpython_ksnip_20230310-085702.png](https://cdn-learn.adafruit.com/assets/assets/000/119/339/medium640/circuitpython_ksnip_20230310-085702.png?1678460280)

Warning: 

This graph shows the author's usage costs while developing and playtesting an app, a total of $1.27 in API calls.

![circuitpython_Screenshot_2023-03-09_09-29-01.png](https://cdn-learn.adafruit.com/assets/assets/000/119/311/medium640/circuitpython_Screenshot_2023-03-09_09-29-01.png?1678375759)

# Infinite Text Adventure

## Configuring the settings.toml File

This project depends on you adding your WiFi settings and OpenAI API key in order to generate the text adventure.

Plug your CircuitPython board into your computer via a known good data + power USB cable. Your board should show up as a thumb drive in your File Explorer / Finder (depending on your operating system) named **CIRCUITPY**.

Create a file with the name&nbsp; **settings.toml** &nbsp;in the root directory of the&nbsp; **CIRCUITPY** &nbsp;drive.

Edit it to contain the keys `WIFI_SSID`, `WIFI_PASSWORD`, and `OPENAI_API_KEY`. (It's also OK for it to contain other keys)

Your file should look similar to the one shown below:

```auto
OPENAI_API_KEY="sk-b6...kP5"
WIFI_SSID="GuestAP"
WIFI_PASSWORD="i trust u"
```

# Infinite Text Adventure

## Coding the Infinite Adventure

![](https://cdn-learn.adafruit.com/assets/assets/000/119/365/medium800/circuitpython_PXL_20230310_143140668.jpg?1678461927)

## Text Editor

Adafruit recommends using the **Mu** editor for editing your CircuitPython code. You can get more info in [this guide](https://learn.adafruit.com/welcome-to-circuitpython/installing-mu-editor).

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&nbsp; **code.py** &nbsp;file. To get everything you need, click on the&nbsp; **Download Project Bundle** &nbsp;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** &nbsp;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.

![folder](https://adafruit.github.io/Adafruit_Learning_System_Guides/CircuitPython_Zorque_Text_Game_openai.png )

https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/main/CircuitPython_Zorque_Text_Game_openai/code.py

# Infinite Text Adventure

## Code Walkthrough

![](https://cdn-learn.adafruit.com/assets/assets/000/119/366/medium800thumb/circuitpython_ezgif.com-optimize.jpg?1678461945)

There's a substantial amount of code in this project. This page focuses on just a few key parts.

## Customization
```auto
# 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
```

After the required imports (not shown), the first customization section includes touchscreen calibration. You can also temporarily disable use of OpenAI, which is useful when testing changes to the program's UI (it's quicker and doesn't make API calls that can cost you money).

The API key is retrieved from the **settings.toml** file on the **CIRCUITPY** drive.

A 10-point or 14-point font is selected depending on the display resolution (14-point for the Titano, 10-point for the original and Pynt). This particular font needs a reduced line spacing. You can substitute another font of your choice, but you may also need to change the `line_spacing` at the same time.

## Creating the game scenario through the base prompt
```auto
# 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.
"""
```

This block of text, which is sent to OpenAI with each completion request, sets the ground rules of the game. I crafted it by trial and error, especially when it came to the problem of how to keep the API from producing too much text to fit on the PyPortal screen. Of course, since things aren't clear cut like programming a computer in a traditional language, it's impossible to set a precise character limit; even "1 paragraph" and "be relatively terse" are treated as guidelines at best.

Want to play a different kind of game? You don't have to code in a computer language, you just write it here within the base prompt in plain English.

Feel like a sci-fi game? Try changing out part of the prompt like so:

> The game is set on an unexplored alien planet. The goal is to repair the ship's Zorque Drive and escape.
> 
> The planet is dangerous and the player's survival is not assured! However, a fresh body is always available from the ship's cloning vat.

Or why not some light fantasy?

> The player is a Princess and her task is to save the kingdom from a dragon that has taken up residence on a nearby hillside. (not necessarily by force, the Princess is more creative than violent)

You can even translate the whole game into another language by simply translating the prompt, done here by Google Translate:

> Vous êtes une IA aidant le joueur à jouer à un jeu d'aventure textuel sans fin. Vous resterez dans votre personnage en tant que MJ.
> 
> Le but du jeu est de sauver le manoir Zorque de la démolition. Le jeu commence à l'extérieur du manoir abandonné de Zorque.  
> &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;  
> En tant que MJ, ne laissez jamais le joueur mourir; il survivent toujours à une situation, aussi pénible soit-elle.
> 
> A chaque étape :  
> &nbsp; &nbsp; &nbsp;\* Offrir une brève description de mon environnement (1 paragraphe)  
> &nbsp; &nbsp; &nbsp;\* Énumérez les articles que je transporte, le cas échéant  
> &nbsp; &nbsp; &nbsp;\* Offrez-moi 4 choix d'action numérotés succincts (1 ou 2 mots chacun)
> 
> Dans tous les cas, soyez relativement concis et gardez un petit nombre de mots.  
> &nbsp; &nbsp;&nbsp;  
> Si le joueur gagne (ou perd), recommencez une nouvelle partie.

## Interacting with the OpenAI "chat completion" API
```auto
# Track the game so far. Always start with the base prompt.
session = [
        {"role": "system", "content": base_prompt.strip()},
]

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]
```

The list `session` keeps track of the "state of the game". It consists of:

- One 'system' message, containing the base prompt
- A limited number of the most recent exchanges between the "user" and the "assistant"

The "slice deletion" `del session[1:-5]` removes items from the middle of the list, keeping the initial item (item 0) and up to 5 items at the end of the list. This means that anything that happened more than 3 "moves" ago is forgotten by the game (because each "move" is two items, a "user" item and an "assistant" item; the 5 preserved items will be 3 "assistant" responses with two "user" actions). This is necessary both because there's an absolute upper limit to the size of a prompt, and because calling the completion API is more costly the longer the prompt is.

To create a full prompt, the user's choice is added to a copy of the session.

```auto
def get_one_completion(full_prompt):
    if not use_openai:
        return f"""This is a canned response in offline mode…""".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()
```

This function takes a "full prompt" which the Python list returned by `make_full_prompt` above.

If use of OpenAI is disabled, a canned response is returned instead of querying the API.

Otherwise, the request is sent to the API endpoint using the `adafruit_requests` library. The API key is included for authorization.

Various kinds of errors are handled; in case of almost any error, `None` is returned. Elsewhere in the code, the completion will be attempted 3 times before giving up.

When the request is successful, the text response is at `j["choices"][0]["message"]["content"]`, and any whitespace at the start or end is removed from the response.

For more about the Chat Completion API, OpenAI provides their own [guide](https://platform.openai.com/docs/guides/chat/introduction) and [API reference](https://platform.openai.com/docs/api-reference/chat/create). There are many other APIs provided by OpenAI, and they can be accessed by changing the post URL and post data appropriately.

```auto
def run_game_step(forced_choice=None):
    if forced_choice:
        choice = forced_choice
    else:
        choice = get_touchscreen_choice()
    print_wrapped(f"\n\nPLAYER: {choice}")
    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)
    terminal.write(clear)
    print_wrapped(result)

    record_game_step(choice, result)
```

This function performs one step within the game. It can optionally take a 'forced choice' which is used to make the player request a 'new game' at the start of every session. Otherwise, the function `get_touchscreen_choice` returns a number from 1 to 4 depending on where the PyPortal's screen is touched. (this function, not shown, also handles vertically scrolling the screen in case it doesn't all fit at once) The OpenAI API call is retried up to 3 times in the case of network problems.

```auto
run_game_step("New game")
while True:
    run_game_step()
```

And playing the game consists of simply doing a step of the game forever! (together with some more error handling code, not shown)


## Featured Products

### Adafruit PyPortal Titano

[Adafruit PyPortal Titano](https://www.adafruit.com/product/4444)
The **PyPortal Titano** is the big sister to our [popular PyPortal](https://www.adafruit.com/product/4116) now with _twice as many pixels!_ The PyPortal is our easy-to-use IoT device that allows you to create all the things for the “Internet of...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4444)
[Related Guides to the Product](https://learn.adafruit.com/products/4444/guides)
### Pink and Purple Woven USB A to USB C Cable - 1 meter long

[Pink and Purple Woven USB A to USB C Cable - 1 meter long](https://www.adafruit.com/product/5153)
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also made for USB C for our modernized breakout boards, Feathers, and more.&nbsp;&nbsp;[If you want something just like it but for Micro B, we...](https://www.adafruit.com/product/4111)

Out of Stock
[Buy Now](https://www.adafruit.com/product/5153)
[Related Guides to the Product](https://learn.adafruit.com/products/5153/guides)
### Adafruit PyPortal - CircuitPython Powered Internet Display

[Adafruit PyPortal - CircuitPython Powered Internet Display](https://www.adafruit.com/product/4116)
 **PyPortal** , our easy-to-use IoT device that allows you to create all the things for the “Internet of Things” in minutes. Make custom touch screen interface GUIs, all open-source, and Python-powered using&nbsp;tinyJSON / APIs to get news, stock, weather, cat photos,...

Out of Stock
[Buy Now](https://www.adafruit.com/product/4116)
[Related Guides to the Product](https://learn.adafruit.com/products/4116/guides)
### Adafruit PyPortal Pynt - CircuitPython Powered Internet Display

[Adafruit PyPortal Pynt - CircuitPython Powered Internet Display](https://www.adafruit.com/product/4465)
The **PyPortal Pynt** is the little&nbsp;sister to our [popular PyPortal](https://www.adafruit.com/product/4116) - zapped with a shrink ray to take the design from a 3.2" diagonal down to 2.4" diagonal screen - but otherwise the same! The PyPortal is&nbsp;our...

No Longer Stocked
[Buy Now](https://www.adafruit.com/product/4465)
[Related Guides to the Product](https://learn.adafruit.com/products/4465/guides)
### Fully Reversible Pink/Purple USB A to micro B Cable - 1m long

[Fully Reversible Pink/Purple USB A to micro B Cable - 1m long](https://www.adafruit.com/product/4111)
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also fully reversible! That's right, you will save _seconds_ a day by not having to flip the cable around.

First let's talk about the cover and over-molding. We got these...

In Stock
[Buy Now](https://www.adafruit.com/product/4111)
[Related Guides to the Product](https://learn.adafruit.com/products/4111/guides)

## Related Guides

- [Adafruit PyPortal - IoT for CircuitPython](https://learn.adafruit.com/adafruit-pyportal.md)
- [Adafruit PyPortal Titano](https://learn.adafruit.com/adafruit-pyportal-titano.md)
- [Playing Animated GIF Files in CircuitPython](https://learn.adafruit.com/using-animated-gif-files-in-circuitpython.md)
- [PyPortal Weather Station](https://learn.adafruit.com/pyportal-weather-station.md)
- [PyPortal GitHub Stars Trophy](https://learn.adafruit.com/pyportal-github-stars-trophy.md)
- [Program in Logo on an Apple II](https://learn.adafruit.com/program-logo-on-an-apple-ii.md)
- [PyPortal WFH Busy Sounds Simulator](https://learn.adafruit.com/pyportal-wfh-busy-sounds-simulator.md)
- [PyPortal Google Calendar Event Display](https://learn.adafruit.com/pyportal-google-calendar-event-display.md)
- [Using LittlevGL with Adafruit Displays](https://learn.adafruit.com/using-littlevgl-with-adafruit-displays.md)
- [Creating Slideshows in CircuitPython](https://learn.adafruit.com/creating-slideshows-in-circuitpython.md)
- [A Logger for CircuitPython](https://learn.adafruit.com/a-logger-for-circuitpython.md)
- [Custom Fonts for CircuitPython Displays](https://learn.adafruit.com/custom-fonts-for-pyportal-circuitpython-display.md)
- [TFT Spirit Board](https://learn.adafruit.com/tft-spirit-board.md)
- [PyPortal IoT Weather Station](https://learn.adafruit.com/pyportal-iot-weather-station.md)
- [PyPortal US Election Calendar](https://learn.adafruit.com/pyportal-electioncal-us.md)
- [PyPortal Winamp MP3 Player](https://learn.adafruit.com/pyportal-winamp-mp3-player.md)
