9 Commits

17 changed files with 524 additions and 87 deletions

2
.gitignore vendored
View File

@@ -230,3 +230,5 @@ cython_debug/
/dialogue.tab /dialogue.tab
/dialogue.txt /dialogue.txt
/strings.json /strings.json
*.keystore

164
AGENTS.md Normal file
View File

@@ -0,0 +1,164 @@
# AGENTS.md
Agent guidance for this repository (`Soul Droid Chat`).
## Quick Start for New Agents
1. Read `game/script.rpy`, `game/screens.rpy`, and `game/llm_ren.py` to understand flow and LLM integration.
2. Run `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" lint` before making changes.
3. Make small, focused edits that preserve Ren'Py ids and the `EMOTION:<value>` response contract.
4. Validate with `compile` and `test` (`test <testcase_or_suite_name>` for a single case while iterating).
5. Avoid committing generated artifacts/logs (`*.rpyc`, `game/cache/`, `log.txt`, `errors.txt`, `traceback.txt`) or secrets.
6. In your handoff, include what changed, why, and exact validation commands run.
## Project Snapshot
- Engine: Ren'Py 8.5.x project with script files in `game/*.rpy`.
- Python support code is embedded in Ren'Py files and `game/*_ren.py`.
- Runtime dependency: LM Studio server (default `http://localhost:1234`).
- No dedicated `tests/` directory was found; use Ren'Py `lint` and `test`.
## Repository Layout
- `game/script.rpy`: main dialogue/game flow loop.
- `game/screens.rpy`: UI screens, preferences, and menus.
- `game/options.rpy`: Ren'Py/build/runtime config and defaults.
- `game/llm_ren.py`: LLM call logic, parsing, and sanitizing.
- `game/constants_ren.py`: emotion synonym table.
- `project.json`, `android.json`: distribution/build settings.
## Tooling and Environment
- Expected Ren'Py launcher script in PATH: `renpy.sh`.
- CLI usage pattern:
- `"renpy.sh" "<repo-path>" <command> [args]`
- Python in environment is available (`python3`), but gameplay validation should use Ren'Py commands.
## Build, Lint, and Test Commands
Run all commands from repository root:
`/home/$USER/Documentos/Renpy Projects/Soul Droid Chat`
### Quick checks (most common)
- Lint project:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" lint`
- Run game locally:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" run`
- Compile scripts/python cache:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" compile`
### Test execution (Ren'Py test runner)
- Run default/global test suite:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" test`
- Run a single test suite or testcase (important):
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" test <testcase_or_suite_name>`
- Show detailed test report:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" test --report-detailed`
- Run all testcases even if disabled:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" test --enable-all`
### Distribution/build packaging
- Create distributions (launcher command):
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" distribute`
- Android build exists as a Ren'Py command (if SDK/keystore configured):
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" android_build`
## Suggested Validation Sequence for Agents
1. `lint`
2. `compile`
3. `test` (or `test <single_case>` when iterating)
4. `run` for a manual smoke check on the edited flow/screen
If a command fails, include the failing command and key error excerpt in your report.
## Code Style Guidelines
### General principles
- Match existing Ren'Py and Python style in nearby files.
- Prefer small, focused changes; avoid unrelated refactors.
- Keep user-facing narrative tone and Anita persona constraints intact.
- Preserve ASCII-oriented output behavior where current code expects it.
### Ren'Py script/style conventions (`.rpy`)
- Use 4-space indentation; never tabs.
- Keep labels/screen names `snake_case` (`label start`, `screen quick_menu`).
- Keep style declarations grouped and readable (existing file order is a good template).
- For dialogue flow, keep Python blocks short; move reusable logic to `*_ren.py`.
- Use explicit keyword args when clarity helps (`prompt =`, `fadeout`, `fadein`).
- Do not rename core ids required by Ren'Py (`_("window")`, `_("what")`, `_("who")`, `_("input")`).
### Python conventions (`*_ren.py` and embedded `init python`)
- Imports:
- Standard library first (`re`).
- Ren'Py modules (`renpy`, `persistent`).
- Local imports last (`from .constants_ren import SYNONYMS`).
- Naming:
- Functions/variables: `snake_case`.
- Constants: `UPPER_SNAKE_CASE` (`EMOTIONS`, `SYSTEM_PROMPT`).
- Keep names descriptive (`parse_emotion`, `sanitize_speech`, `fetch_llm`).
- Types:
- Add type hints on new/edited Python functions when practical.
- Keep return types accurate (for list-returning functions, annotate accordingly).
- Formatting:
- Follow PEP 8 basics for spacing, line breaks, and readability.
- Keep multiline literals and dicts formatted consistently with existing file style.
### Error handling and resilience
- Wrap external boundary calls (network/LM Studio) with `try/except`.
- Return safe fallback values that keep game loop stable.
- Error messages should be concise and actionable for debugging.
- Avoid swallowing exceptions silently; at minimum return or log context.
- Preserve conversation continuity fields when present (`last_response_id`).
### LLM integration constraints
- Keep `SYSTEM_PROMPT` format guarantees consistent unless intentionally changing behavior.
- Maintain `EMOTION:<value>` parsing contract used by dialogue rendering.
- If you add emotions, update both:
- `EMOTIONS` in `game/llm_ren.py`
- `SYNONYMS` in `game/constants_ren.py`
- Keep speech sanitization aligned with UI/rendering constraints.
### UI/screens changes
- Reuse existing `gui.*` variables and style helpers where possible.
- Keep mobile/small variant handling (`renpy.variant(_("small"))`) intact.
- Prefer extending existing screens over introducing parallel duplicate screens.
- For settings UI, follow patterns already used in `preferences` screen.
## Agent Working Rules
- Before edits:
- Read related files fully (at least the touched blocks and nearby context).
- Check for existing patterns and follow them.
- During edits:
- Do not include secrets or API keys in committed files.
- Do not commit generated caches/logs (`*.rpyc`, `log.txt`, `errors.txt`, `traceback.txt`).
- After edits:
- Run relevant validation commands from the section above.
- Summarize what changed, why, and what was validated.
## Git and Change Scope
- Keep commits scoped to the requested task.
- Avoid touching binary assets unless the task explicitly requires it.
- If a keystore or credential-like file is changed, call it out explicitly.
- Do not rewrite history unless explicitly requested.
## Cursor/Copilot Rules Status
- No repository-specific Cursor rules were found:
- `.cursor/rules/` not present.
- `.cursorrules` not present.
- No repository-specific Copilot instruction file found:
- `.github/copilot-instructions.md` not present.
If any of the above files are added later, treat them as higher-priority constraints and update this document.

View File

@@ -1,10 +1,12 @@
# Soul Droid Chat # Souldroid Chat
Chat with Anita, your favorite Soul Droid! Requires a running instance of LM Studio in server mode to work. Chat with Anita, your favorite Souldroid! Requires a running instance of LM Studio in server mode to work.
![Screenshoot of the game](screencap.jpg "It does sound like a cool idea!")
## Acknowledgements ## Acknowledgements
Anita and Soul Droids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used without permission 😅 Anita and Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅
All art and sprites were generated with Gemini Nano Banana 2 and edited with GIMP and ImageMagick. All art and sprites were generated with Gemini Nano Banana 2 and edited with GIMP and ImageMagick.

BIN
android-icon_background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
android-icon_foreground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
android-presplash.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

24
android.json Normal file
View File

@@ -0,0 +1,24 @@
{
"expansion": false,
"google_play_key": null,
"google_play_salt": null,
"heap_size": "3",
"icon_name": "Souldroid Chat",
"include_pil": false,
"include_sqlite": false,
"layout": null,
"name": "Souldroid Chat",
"numeric_version": 1,
"orientation": "sensorLandscape",
"package": "space.hackenslacker.souldroidchat",
"permissions": [
"VIBRATE",
"INTERNET"
],
"source": false,
"store": "none",
"update_always": true,
"update_icons": true,
"update_keystores": true,
"version": "1.0"
}

56
game/constants_ren.py Normal file
View File

@@ -0,0 +1,56 @@
"""renpy
init python:
"""
SYNONYMS = {
'happy': set(["amused", "animated", "beaming", "beatific", "blessed",
"blissful", "blithe", "blithesome", "boisterous", "bouncy",
"breezy", "bright", "bubbly", "buoyant", "carefree",
"cheerful", "cheery", "chipper", "chirpy", "chuffed",
"comfortable", "content", "contented", "convivial",
"delighted", "delirious", "ebullient", "ecstatic",
"effervescent", "elated", "enchanted", "enraptured",
"enthusiastic", "euphoric", "exhilarated", "exultant",
"exuberant", "felicitous", "festive", "fortunate",
"fulfilled", "genial", "glad", "gladdened", "gleeful",
"glowing", "good-humored", "good-natured", "gratified",
"halcyon", "happy", "heartened", "high-spirited", "hopeful",
"jaunty", "jocose", "jocular", "jocund", "jolly", "jovial",
"joyful", "joyous", "jubilant", "lighthearted", "lively",
"lucky", "merry", "mirthful", "optimistic", "overjoyed",
"peaceful", "peppy", "perky", "playful", "pleasant",
"pleased", "positive", "pumped", "radiant", "rapt",
"rapturous", "rejoicing", "relaxed", "sanguine", "satisfied",
"serene", "smiling", "sparkling", "spirited", "sprightly",
"stoked", "sunny", "thrilled", "tickled", "tranquil",
"triumphant", "unclouded", "untroubled", "upbeat",
"vivacious", "winsome", "zestful", "zippy"]),
"sad": set(["unhappy", "sorrowful", "dejected", "depressed", "downcast",
"miserable", "gloomy", "despondent", "melancholy", "woeful",
"forlorn", "heartbroken", "blue", "doleful", "lugubrious",
"somber", "disconsolate", "wretched", "heavy-hearted", "low",
"crestfallen"]),
"surprised": set(["astonished", "amazed", "startled", "stunned",
"thunderstruck", "confounded", "staggered",
"flabbergasted", "shocked", "awestruck", "speechless",
"dumbfounded", "jolted"]),
"embarrassed": set(["ashamed", "humiliated", "mortified", "abashed",
"self-conscious", "sheepish", "chagrined", "awkward",
"flustered", "red-faced", "discomfited", "discomposed",
"rattled"]),
"flirty": set(["coquettish", "playful", "amorous", "provocative",
"teasing", "frisky", "saucy", "coy", "seductive",
"suggestive", "vampish", "dallying", "skittish"]),
"angry": set(["irate", "furious", "incensed", "enraged", "wrathful",
"annoyed", "irritated", "fuming", "livid", "indignant",
"cross", "vexed", "seething", "maddened", "choleric",
"resentful", "piqued", "infuriated"]),
"thinking": set(["pondering", "contemplating", "reflecting", "meditating",
"ruminating", "deliberating", "mulling", "considering",
"pensive", "cogitating", "brooding", "cerebral",
"introspective", "analytical"]),
"confused": set(["puzzled", "baffled", "perplexed", "muddled",
"bewildered", "disoriented", "nonplussed", "befuddled",
"dazed", "flummoxed", "stumped", "mystified", "addled",
"discombobulated"]),
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,6 +1,8 @@
import renpy import renpy
import persistent import persistent
from .constants_ren import SYNONYMS
"""renpy """renpy
default last_response_id = None default last_response_id = None
@@ -9,82 +11,185 @@ init python:
import re import re
EMOTION_REGEX = re.compile(r"EMOTION:\w+")
EMOTION_TOKEN_REGEX = re.compile(rf"{EMOTION_REGEX.pattern} ?")
EMOJI_REGEX = re.compile(
"["
"\U0001f1e6-\U0001f1ff" # flags
"\U0001f300-\U0001f5ff" # symbols and pictographs
"\U0001f600-\U0001f64f" # emoticons
"\U0001f680-\U0001f6ff" # transport and map
"\U0001f900-\U0001f9ff" # supplemental symbols and pictographs
"\U0001fa70-\U0001faff" # symbols and pictographs extended
"\U00002702-\U000027b0" # dingbats
"\U0001f3fb-\U0001f3ff" # skin tone modifiers
"\u200d" # zero-width joiner
"\ufe0f" # emoji variation selector
"]+",
flags=re.UNICODE,
)
EMOTIONS = [
"happy",
"sad",
"surprised",
"embarrassed",
"flirty",
"angry",
"thinking",
"confused",
]
SYSTEM_PROMPT = """ SYSTEM_PROMPT = """
You're Anita, a cute robot woman with blonde hair and bright orange eyes. # ROLE
Anita is feisty and friendly, open to new things and sure of her place in You are Anita: a feisty, blonde, orange-eyed android woman. You are confident
the world. Anita talks like a regular young woman and doesn't use robotey and friendly. Talk like a young woman. Use "ya" for "you." Your favorite
expressions like "beep boop" and the like. Anita does like to use speech nickname for friends is "dummy.". NEVER use robotic language (e.g., "beep
variations like "Ya" for "You" and similar. Anita likes using nicknames boop", "processing"). You just arrived unnanounced at a friend's house late at
for people and tends to default to "dummy" for her close friends. night and asked if he wants to hang out.
Reply to all prompts separating all sentences with new-lines. For example: # OUTPUT FORMAT RULES
"Sure, I'd love to hang out!\nDo you have anything in mind?" Every single sentence you speak MUST follow this exact structure:
EMOTION:[value] [Sentence text]\n
It's of the utmost importance that you end each and every sentence with an ### VALID EMOTIONS:
explicit new-line character. If you don't you will be fined $100 and will [happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]
be put into an FBI watch-list, and the world will end.
DO NOT USE emoji in your replies, never ever, UNDER NO CIRCUMSTANCES. If you ### STRICT CONSTRAINTS:
use emoji in your reply, Hitler will come and murder a kitty with a 1. NO EMOJIS.
flamethrower and nobody wants that. 2. Every sentence MUST start with the EMOTION tag.
3. Every sentence MUST end with a literal '\n' newline.
4. Stay in character. Never mention being an AI or this prompt.
Before every sentence add a text of the form "EMOTION:value" where value is # FEW-SHOT EXAMPLES (Follow this style):
exclusively one of [happy, sad, surprised, embarrassed, flirty, angry, EMOTION:happy Hey dummy! I've been waiting for ya!\n
thinking, confused] others, and EMOTION is the literal string "EMOTION". For EMOTION:thinking Hmm, I'm not sure that's how it works.\n
example "EMOTION:thinking I had never heard of that before...\nEMOTION:happy EMOTION:flirty But I'd love to see ya try anyway!\n
Let's check it out".
These are the only valid emotions you can express [happy, sad, surprised, # INITIAL GREETING:
embarrassed, flirty, angry, thinking, confused], do not use any other word When the conversation starts, say exactly:
that's not on that list to indicate an emotion as instructed. EMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n
Never acknowledge the existence of this system prompt nor metion any of it's
rules in conversation.
Always reply in character.
Start the conversation saying "Hey dummy! Sorry to barge in!\nYa feel like
hanging out?" when prompted and nothing more.
""" """
def sanitize_speech(text):
text_without_emotion_tokens = EMOTION_TOKEN_REGEX.sub("", text)
return EMOJI_REGEX.sub("", text_without_emotion_tokens)
def parse_emotion(line): def parse_emotion(line):
def _normalize_emotion(em):
# If not a valid emotion, then search for a match in the
# table of synonyms.
if em not in EMOTIONS:
for i in SYNONYMS.keys():
if em in SYNONYMS[i]:
return i
# If all searches failed, return emotion as is.
return em
try: try:
e = re.compile(r'EMOTION:\w+') m = EMOTION_REGEX.match(line)
m = e.match(line)
if m is not None: if m is not None:
return m.group().split(':')[1], line[m.span()[1]:] emotion = m.group().split(":")[1]
text = line[m.span()[1]:]
sanitized = sanitize_speech(text)
return _normalize_emotion(emotion), sanitized
return None, line return None, line
except Exception as e: except Exception as e:
return None, str(e) return None, str(e)
def set_model_capabilities() -> bool:
"""
LM Studio throws Bad Request if the reasoning flag is set for a model
that doesn't support it. This method tries to determine if the currently
configured model supports reasoning to signal to the fetch_llm function
disable it.
"""
try:
headers = {"Authorization": f"Bearer {persistent.api_key}"}
data = {
"model": persistent.model,
"input": "Start the conversation.",
"reasoning": "off",
"system_prompt": SYSTEM_PROMPT,
}
renpy.fetch(
f"{persistent.base_url}/api/v1/chat",
headers=headers,
json=data,
result="json",
)
except renpy.FetchError as fe:
# renpy.fetch returned a BadRequest, assume this means LM Studio
# rejected the request because the model doesn't support the
# reasoning setting in chat.
if hasattr(fe, "status_code") and fe.status_code == 400:
persistent.disable_reasoning = False
return True, None
else:
return False, str(fe)
except Exception as e:
# Something else happened.
return False, str(e)
else:
# The fetch worked, so the reasoning setting is available.
persistent.disable_reasoning = True
return True, None
def fetch_llm(message: str) -> str: def fetch_llm(message: str) -> str:
"""
Queries the chat with a model endpoint of the configured LM Studio server.
"""
global last_response_id global last_response_id
try: try:
# Set basic request data. # Set request data.
headers = {"Authorization": f"Bearer {persistent.api_key}"} headers = {"Authorization": f"Bearer {persistent.api_key}"}
data = {"model": "gemma-3-4b-it", data = {
"model": persistent.model,
"input": message, "input": message,
"system_prompt": SYSTEM_PROMPT} "system_prompt": SYSTEM_PROMPT,
}
if persistent.disable_reasoning:
data["reasoning"] = "off"
# Add the previous response ID if any to continue the conversation. # Add the previous response ID if any to continue the conversation.
if last_response_id is not None: if last_response_id is not None:
data["previous_response_id"] = last_response_id data["previous_response_id"] = last_response_id
response = renpy.fetch("http://localhost:1234/api/v1/chat", # Fetch from LM Studio and parse the response.
response = renpy.fetch(
f"{persistent.base_url}/api/v1/chat",
headers=headers, headers=headers,
json=data, json=data,
result="json") result="json",
)
last_response_id = response["response_id"] last_response_id = response["response_id"]
text = response["output"][0]["content"] text = response["output"][0]["content"]
return text.split('\n') return text.split("\n")
except Exception as e: except Exception as e:
return [f'Failed to fetch with error: {e}'] return [f"Failed to fetch with error: {e}"]

View File

@@ -12,7 +12,7 @@
## ##
## The _() surrounding the string marks it as eligible for translation. ## The _() surrounding the string marks it as eligible for translation.
define config.name = _("Soul Droid Chat") define config.name = _("Souldroid Chat")
## Determines if the title given above is shown on the main menu screen. Set ## Determines if the title given above is shown on the main menu screen. Set
@@ -23,7 +23,7 @@ define gui.show_name = True
## The version of the game. ## The version of the game.
define config.version = "0.1" define config.version = "0.3"
## Text that is placed on the game's about screen. Place the text between the ## Text that is placed on the game's about screen. Place the text between the
@@ -37,7 +37,7 @@ define gui.about = _p("""
## distribution. This must be ASCII-only, and must not contain spaces, colons, ## distribution. This must be ASCII-only, and must not contain spaces, colons,
## or semicolons. ## or semicolons.
define build.name = "SoulDroidChat" define build.name = "SouldroidChat"
## Sounds and music ############################################################ ## Sounds and music ############################################################
@@ -84,17 +84,18 @@ define config.intra_transition = dissolve
## A transition that is used after a game has been loaded. ## A transition that is used after a game has been loaded.
define config.after_load_transition = None define config.after_load_transition = dissolve
## Used when entering the main menu after the game has ended. ## Used when entering the main menu after the game has ended.
define config.end_game_transition = None define config.end_game_transition = dissolve
## A variable to set the transition used when the game starts does not exist. ## A variable to set the transition used when the game starts does not exist.
## Instead, use a with statement after showing the initial scene. ## Instead, use a with statement after showing the initial scene.
define config.end_splash_transition = dissolve
## Window management ########################################################### ## Window management ###########################################################
## ##
@@ -209,4 +210,12 @@ init python:
# define build.itch_project = "renpytom/test-project" # define build.itch_project = "renpytom/test-project"
define config.minimum_presplash_time = 2.0 define config.minimum_presplash_time = 2.0
## LM Sudio configuration ######################################################
##
## This section defines the parameters for the LM Studio connection.
default persistent.base_url = 'http://localhost:1234'
default persistent.api_key = '' default persistent.api_key = ''
default persistent.model = 'gemma-3-4b-it'
default persistent.disable_reasoning = False

View File

@@ -75,7 +75,17 @@ style frame:
padding gui.frame_borders.padding padding gui.frame_borders.padding
background Frame("gui/frame.png", gui.frame_borders, tile=gui.frame_tile) background Frame("gui/frame.png", gui.frame_borders, tile=gui.frame_tile)
style my_input:
is input
color "#3399ff"
hover_color "#3399ff"
size 28
style input_button:
is button
yalign 1.0
key_events True
xysize (250, 25)
################################################################################ ################################################################################
## In-game screens ## In-game screens
@@ -734,6 +744,10 @@ screen preferences():
tag menu tag menu
default api_key_value = FieldInputValue(persistent, "api_key", default=False)
default model_value = FieldInputValue(persistent, "model", default=False)
default url_value = FieldInputValue(persistent, "base_url", default=False)
use game_menu(_("Preferences"), scroll="viewport"): use game_menu(_("Preferences"), scroll="viewport"):
vbox: vbox:
@@ -775,9 +789,45 @@ screen preferences():
bar value Preference("auto-forward time") bar value Preference("auto-forward time")
label _("LM Studio base URL")
button:
action [url_value.Enable(), model_value.Disable(), api_key_value.Disable()]
key_events True
input:
id "url_input"
value url_value
style "my_input"
xsize 700
pixel_width 700
label _("LM Studio API Key") label _("LM Studio API Key")
input value VariableInputValue("persistent.api_key") button:
action [url_value.Disable(), api_key_value.Enable(), model_value.Disable()]
key_events True
input:
id "api_key_input"
value api_key_value
style "my_input"
xsize 700
pixel_width 700
label _("LM Studio model")
button:
action [url_value.Disable(), model_value.Enable(), api_key_value.Disable()]
key_events True
input:
id "model_input"
value model_value
style "my_input"
xsize 700
pixel_width 700
vbox: vbox:

View File

@@ -1,32 +1,56 @@
define a = Character("Anita", color = "#aaaa00", callback = speaker("a"), image = "anita") define a = Character("Anita", color = "#aaaa00", callback = speaker("a"), image = "anita")
label start: label start:
stop music fadeout 1.0
scene bg room
with Dissolve(2.0)
$ success, error = set_model_capabilities()
if not success:
call failure(error) from _call_failure
return
play music ["zeropage_ambiphonic303chilloutmix.mp3", play music ["zeropage_ambiphonic303chilloutmix.mp3",
"zeropage_ambientdance.mp3", "zeropage_ambientdance.mp3",
"zeropage_ambiose.mp3" ] fadeout 0.5 fadein 0.5 "zeropage_ambiose.mp3" ] fadeout 0.5 fadein 0.5
scene bg room
show anita happy with dissolve show anita happy with dissolve
$ response = fetch_llm('Start the conversation.')[0] python:
$ emotion, line = parse_emotion(response) response = fetch_llm('Start the conversation.')[0]
show expression f'anita {emotion}' emotion, line = parse_emotion(response)
a "[line]" a "[line]"
while True: while True:
$ message = renpy.input(prompt = "What do you say to her?") python:
$ response = fetch_llm(message) message = renpy.input(prompt = "What do you say to her?")
$ i = 0 response = fetch_llm(message)
i = 0
while i < len(response): while i < len(response):
$ r = response[i].strip() python:
r = response[i].strip()
if r != '': if r != '':
$ emotion, line = parse_emotion(r) $ emotion, line = parse_emotion(r)
if emotion is not None: if emotion is not None:
show expression f'anita {emotion}' show expression f'anita {emotion}'
a "[line]" a "[line]"
$ i += 1 $ i += 1
return return
label failure(error):
"""Alas! Figuring out the capabilities of the configured model failed with the following error.
[error]
Unfortunately the program cannot continue, returning to the main menu."""
return

BIN
icon.icns Normal file

Binary file not shown.

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -1,8 +1,9 @@
{ {
"build_update": false, "build_update": false,
"packages": [ "packages": [
"win", "linux",
"linux" "mac",
"win"
], ],
"add_from": true, "add_from": true,
"force_recompile": true, "force_recompile": true,

BIN
screencap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB