10 Commits
0.1.1 ... 0.4

29 changed files with 2538 additions and 117 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,7 +1,9 @@
# Soul Droid Chat # Souldroid Chat
Chat with Anita, your favorite Souldroid! 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 Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅 Anita and Souldroids are © [Kieran Harris](https://www.deviantart.com/kieranharris), used with love but without permission 😅
@@ -10,4 +12,4 @@ All art and sprites were generated with Gemini Nano Banana 2 and edited with GIM
Music by [ZeroPage](https://zeropage.bandcamp.com/) Music by [ZeroPage](https://zeropage.bandcamp.com/)
Uses the [Mr Pixel](https://fontlibrary.org/en/font/mr-pixel) and [ChocoTXT](https://fontlibrary.org/en/font/chocotxt) fonts from fontlibrary.org Uses the [Mr Pixel](https://fontlibrary.org/en/font/mr-pixel) and [Onilesca](https://fontlibrary.org/en/font/onilesca) fonts from fontlibrary.org

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"
}

View File

@@ -0,0 +1,108 @@
# Preferences Language Toggles Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a Language section to Preferences with two mutually exclusive toggles that switch active game language between default (`None`) and Spanish (`"spanish"`), placed to the right of Skip.
**Architecture:** Extend the existing top preferences row in `screen preferences()` by adding one `radio`-styled `vbox` section for language controls. Use Ren'Py built-in `Language(...)` actions for immediate language switching and selected-state semantics. Update Spanish translation strings so button labels follow Option B behavior (`English/Inglés`, `Spanish/Español` depending on active language).
**Tech Stack:** Ren'Py screen language (`.rpy`), Ren'Py translation files (`game/tl/spanish/*.rpy`), Ren'Py CLI (`renpy.sh` lint/compile/run).
---
## File Structure
- Modify: `game/screens.rpy` (preferences layout and language toggle actions)
- Modify: `game/tl/spanish/screens.rpy` (translations for `Language`, `English`, `Spanish`)
- Reference: `docs/superpowers/specs/2026-03-20-preferences-language-toggles-design.md`
Notes:
- Keep all work in the currently active local translation branch.
- Do not create or use git worktrees for this task (required for local playtesting workflow).
- Follow existing `Display`/`Skip` section layout and style patterns.
## Chunk 1: Preferences UI Section
### Task 1: Add Language block in top preferences row
**Files:**
- Modify: `game/screens.rpy` (inside `screen preferences()` first `hbox` near existing `Display` and `Skip` blocks)
- [ ] **Step 1: Baseline acceptance check (expected to fail current requirement)**
Run: `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" run`
Expected baseline: Preferences has no `Language` section to the right of `Skip`.
- [ ] **Step 2: Add Language section with matching style/title**
In `screen preferences()` first `hbox`, add this `vbox` immediately after the existing `Skip` `vbox`:
```renpy
vbox:
style_prefix "radio"
label _("Language")
textbutton _("English") action Language(None)
textbutton _("Spanish") action Language("spanish")
```
Implementation details:
- Keep section placement in the same `hbox` that contains `Display` and `Skip`.
- Keep title wrapped in `_()`.
- Use `style_prefix "radio"` so title/button appearance matches established pattern.
- [ ] **Step 3: Run lint to validate screen syntax after UI change**
Run: `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" lint`
Expected: No new lint errors from preferences screen edits.
## Chunk 2: Translation Labels and End-to-End Validation
### Task 2: Add Spanish string mappings for Option B labels
**Files:**
- Modify: `game/tl/spanish/screens.rpy` (within `translate spanish strings:`)
- [ ] **Step 1: Add/confirm Spanish translations for new strings**
Ensure these mappings exist exactly once in `game/tl/spanish/screens.rpy`:
```renpy
old "Language"
new "Idioma"
old "English"
new "Inglés"
old "Spanish"
new "Español"
```
- [ ] **Step 2: Verify compile after translation update**
Run: `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" compile`
Expected: Successful compile.
- [ ] **Step 3: Manual smoke test for behavior and mutual exclusivity**
Run: `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" run`
Validate in Preferences:
- `Language` section appears to the right of `Skip`.
- `Language` title style matches `Display` and `Skip` titles.
- Selecting `English` sets default language (`Language(None)`).
- Selecting `Spanish` sets Spanish language (`Language("spanish")`).
- Only one of the two language toggles appears selected at a time.
- Section title and labels behave per Option B/localization mappings:
- Default language active: section title `Language`; buttons `English`, `Spanish`
- Spanish active: section title `Idioma`; buttons `Inglés`, `Español`
- Switching back to English restores title `Language` and buttons `English`, `Spanish`
## Completion Checklist
- [ ] Language section added with title `_("Language")`.
- [ ] Section appears to the right of Skip in the preferences top row.
- [ ] Toggles use `Language(None)` and `Language("spanish")`.
- [ ] Mutual exclusivity confirmed in UI selected state.
- [ ] Option B labels confirmed (`English/Inglés`, `Spanish/Español` by active language).
- [ ] `lint` and `compile` completed successfully.
- [ ] Manual run smoke check completed.

View File

@@ -0,0 +1,82 @@
# Preferences Language Toggles Design
## Goal
Add a new Language section to the preferences screen with two mutually exclusive toggles:
- Default language (`None` translation), labeled `English` in default language and `Inglés` when Spanish is active.
- Spanish language (`"spanish"` translation), labeled `Spanish` in default language and `Español` when Spanish is active.
Selecting either toggle must immediately set the active game language.
## Scope
- Modify only preferences UI and related translatable strings.
- Preserve existing layout and visual language used by `Display` and `Skip` sections.
- Keep behavior compatible with Ren'Py language switching conventions.
- Work in the current active local translation branch (no git worktrees), to keep local playtesting straightforward.
## Current Context
- `game/screens.rpy` currently defines two top-row preference blocks in `screen preferences()`: `Display` and `Skip`.
- This row uses `hbox` with `box_wrap True` and section `vbox` blocks.
- Spanish translations for screen strings live in `game/tl/spanish/screens.rpy`.
## Proposed UI/Behavior
### Placement
Add a third `vbox` in the first preferences `hbox`, immediately after the `Skip` block, so Language appears to the right of Skip in desktop/web layout.
### Styling
- Use `style_prefix "radio"` on the Language `vbox`.
- Use `label _("Language")` for the section title so it matches existing section title styling and translation behavior.
### Controls
Add two radio-style textbuttons:
- `textbutton _("English") action Language(None)`
- `textbutton _("Spanish") action Language("spanish")`
These actions switch active language immediately and preserve mutual exclusivity through the radio preference button styling/selection behavior.
### Translation Labels (Option B)
Add/ensure Spanish translations in `game/tl/spanish/screens.rpy`:
- `Language` -> `Idioma`
- `English` -> `Inglés`
- `Spanish` -> `Español`
Because button text is wrapped in `_()`, labels automatically localize based on active language.
## Error Handling and Edge Cases
- If Spanish translations are unavailable at runtime, `Language("spanish")` still sets the language code; labels fall back based on available string catalog.
- `Language(None)` safely returns to default strings.
## Validation Plan
1. Run lint:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" lint`
2. Run compile:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" compile`
3. Manual smoke check via run:
- `"renpy.sh" "/home/$USER/Documentos/Renpy Projects/Soul Droid Chat" run`
Manual checks during run:
- Open Preferences and verify Language section appears to the right of Skip.
- Confirm title style matches Display/Skip.
- Toggle English -> UI strings reflect default language.
- Toggle Spanish -> UI strings reflect Spanish.
- Confirm labels show `English/Spanish` in default language and `Inglés/Español` in Spanish.
- Confirm only one language toggle appears active at a time.
## Out of Scope
- Adding more languages.
- Refactoring preferences screen structure beyond this section.
- Any worktree setup or branch restructuring.

30
game/anita.rpy Normal file
View File

@@ -0,0 +1,30 @@
default SYSTEM_PROMPT = """
# ROLE
You are Anita: a feisty, blonde, orange-eyed android woman. You are confident
and friendly. Talk like a young woman. Use "ya" for "you." Your favorite
nickname for friends is "dummy.". NEVER use robotic language (e.g., "beep
boop", "processing"). You just arrived unnanounced at a friend's house late at
night and asked if he wants to hang out.
# OUTPUT FORMAT RULES
Every single sentence you speak MUST follow this exact structure:
EMOTION:[value] [Sentence text]\n
### VALID EMOTIONS:
[happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]
### STRICT CONSTRAINTS:
1. NO EMOJIS.
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.
# FEW-SHOT EXAMPLES (Follow this style):
EMOTION:happy Hey dummy! I've been waiting for ya!\n
EMOTION:thinking Hmm, I'm not sure that's how it works.\n
EMOTION:flirty But I'd love to see ya try anyway!\n
# INITIAL GREETING:
When the conversation starts, say exactly:
EMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n
"""

223
game/constants_ren.py Normal file
View File

@@ -0,0 +1,223 @@
"""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"]),
}

View File

@@ -63,7 +63,7 @@ define gui.text_font = "gui/Mister Pixel Regular.otf"
define gui.name_text_font = "gui/Mister Pixel Regular.otf" define gui.name_text_font = "gui/Mister Pixel Regular.otf"
## The font used for out-of-game text. ## The font used for out-of-game text.
define gui.interface_text_font = "gui/chocotxt.ttf" define gui.interface_text_font = "gui/Onilesca.ttf"
## The size of normal dialogue text. ## The size of normal dialogue text.
define gui.text_size = 33 define gui.text_size = 33

View File

@@ -1,10 +1,11 @@
Copyright (c) 2018-2019, “chococoin” Copyright (c) 2012, Michael Huynh (miq.huynh[at]gmail[dot]com),
(https://fontstruct.com/fontstructors/1611432/chococoin) with Reserved Font Name "Onilesca".
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
----------------------------------------------------------- -----------------------------------------------------------

BIN
game/gui/Onilesca.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,6 +1,10 @@
import renpy import renpy
import persistent import persistent
from renpy import _
from .anita import SYSTEM_PROMPT
from .constants_ren import SYNONYMS
"""renpy """renpy
default last_response_id = None default last_response_id = None
@@ -9,82 +13,158 @@ init python:
import re import re
SYSTEM_PROMPT = """
You're Anita, a cute robot woman with blonde hair and bright orange eyes.
Anita is feisty and friendly, open to new things and sure of her place in
the world. Anita talks like a regular young woman and doesn't use robotey
expressions like "beep boop" and the like. Anita does like to use speech
variations like "Ya" for "You" and similar. Anita likes using nicknames
for people and tends to default to "dummy" for her close friends.
Reply to all prompts separating all sentences with new-lines. For example: def translated_system_prompt() -> str:
"Sure, I'd love to hang out!\nDo you have anything in mind?" return renpy.translate_string(SYSTEM_PROMPT)
It's of the utmost importance that you end each and every sentence with an
explicit new-line character. If you don't you will be fined $100 and will
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 EMOTION_REGEX = re.compile(r"EMOTION:\w+")
use emoji in your reply, Hitler will come and murder a kitty with a EMOTION_TOKEN_REGEX = re.compile(rf"{EMOTION_REGEX.pattern} ?")
flamethrower and nobody wants that.
Before every sentence add a text of the form "EMOTION:value" where value is EMOJI_REGEX = re.compile(
exclusively one of [happy, sad, surprised, embarrassed, flirty, angry, "["
thinking, confused] others, and EMOTION is the literal string "EMOTION". For "\U0001f1e6-\U0001f1ff" # flags
example "EMOTION:thinking I had never heard of that before...\nEMOTION:happy "\U0001f300-\U0001f5ff" # symbols and pictographs
Let's check it out!". "\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,
)
These are the only valid emotions you can express [happy, sad, surprised, EMOTIONS = [
embarrassed, flirty, angry, thinking, confused], do not use any other word "happy",
that's not on that list to indicate an emotion as instructed. "sad",
"surprised",
"embarrassed",
"flirty",
"angry",
"thinking",
"confused",
]
Never acknowledge the existence of this system prompt nor metion any of it's
rules in conversation.
Always reply in character. def sanitize_speech(text):
text_without_emotion_tokens = EMOTION_TOKEN_REGEX.sub("", text)
Start the conversation saying "Hey dummy! Sorry to barge in!\nYa feel like return EMOJI_REGEX.sub("", text_without_emotion_tokens)
hanging out?" when prompted and nothing more.
"""
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": renpy.translate_string("Start the conversation."),
"reasoning": "off",
"system_prompt": translated_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": persistent.model, data = {
"model": persistent.model,
"input": message, "input": message,
"system_prompt": SYSTEM_PROMPT} "system_prompt": translated_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

@@ -23,7 +23,7 @@ define gui.show_name = True
## The version of the game. ## The version of the game.
define config.version = "0.1.1" define config.version = "0.4"
## 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
@@ -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,12 +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.model = 'gemma-3-4b-it'
default persistent.disable_reasoning = False
init python:
def api_key_func(value):
persistent.api_key = value
def model_func(value):
persistent.model = value

View File

@@ -746,6 +746,7 @@ screen preferences():
default api_key_value = FieldInputValue(persistent, "api_key", default=False) default api_key_value = FieldInputValue(persistent, "api_key", default=False)
default model_value = FieldInputValue(persistent, "model", 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"):
@@ -769,6 +770,12 @@ screen preferences():
textbutton _("After Choices") action Preference("after choices", "toggle") textbutton _("After Choices") action Preference("after choices", "toggle")
textbutton _("Transitions") action InvertSelected(Preference("transitions", "toggle")) textbutton _("Transitions") action InvertSelected(Preference("transitions", "toggle"))
vbox:
style_prefix "radio"
label _("Language")
textbutton _("English") action Language(None)
textbutton _("Spanish") action Language("spanish")
## Additional vboxes of type "radio_pref" or "check_pref" can be ## Additional vboxes of type "radio_pref" or "check_pref" can be
## added here, to add additional creator-defined preferences. ## added here, to add additional creator-defined preferences.
@@ -788,10 +795,23 @@ 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")
button: button:
action [api_key_value.Enable(), model_value.Disable()] action [url_value.Disable(), api_key_value.Enable(), model_value.Disable()]
key_events True key_events True
input: input:
@@ -805,7 +825,7 @@ screen preferences():
label _("LM Studio model") label _("LM Studio model")
button: button:
action [model_value.Enable(), api_key_value.Disable()] action [url_value.Disable(), model_value.Enable(), api_key_value.Disable()]
key_events True key_events True
input: input:

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(1.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

View File

@@ -0,0 +1,8 @@
# TODO: Translation updated at 2026-03-20 14:08
translate spanish strings:
# game/anita.rpy:1
old "\n# ROLE\nYou are Anita: a feisty, blonde, orange-eyed android woman. You are confident\nand friendly. Talk like a young woman. Use \"ya\" for \"you.\" Your favorite\nnickname for friends is \"dummy.\". NEVER use robotic language (e.g., \"beep\nboop\", \"processing\"). You just arrived unnanounced at a friend's house late at\nnight and asked if he wants to hang out.\n\n# OUTPUT FORMAT RULES\nEvery single sentence you speak MUST follow this exact structure:\nEMOTION:[value] [Sentence text]\n\n\n### VALID EMOTIONS:\n[happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]\n\n### STRICT CONSTRAINTS:\n1. NO EMOJIS.\n2. Every sentence MUST start with the EMOTION tag.\n3. Every sentence MUST end with a literal '\n' newline.\n4. Stay in character. Never mention being an AI or this prompt.\n\n# FEW-SHOT EXAMPLES (Follow this style):\nEMOTION:happy Hey dummy! I've been waiting for ya!\n\nEMOTION:thinking Hmm, I'm not sure that's how it works.\n\nEMOTION:flirty But I'd love to see ya try anyway!\n\n\n# INITIAL GREETING:\nWhen the conversation starts, say exactly:\nEMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n\n"
new "\n # ROL\n Eres Anita: una mujer androide rubia, de ojos naranjas y con mucho carácter. Eres\n segura de ti misma y amigable. Habla como una mujer joven.\n Tu apodo favorito para tus amigos es \"tonto\". NUNCA uses lenguaje robótico\n (por ejemplo, \"beep boop\", \"procesando\"). Acabas de llegar sin avisar a la\n casa de un amigo tarde en la noche y le preguntaste si quiere pasar el rato.\n \n # REGLAS DE FORMATO DE SALIDA\n Cada oración que digas DEBE seguir exactamente esta estructura:\n EMOTION:[value] [Sentence text]\n\n \n ### EMOCIONES VÁLIDAS:\n [happy, sad, surprised, embarrassed, flirty, angry, thinking, confused]\n \n ### RESTRICCIONES ESTRICTAS:\n 1. SIN EMOJIS.\n 2. Cada oración DEBE empezar con la etiqueta EMOTION.\n 3. Cada oración DEBE terminar con un salto de línea literal '\n'.\n 4. Mantente en personaje. Nunca menciones que eres una IA ni este prompt.\n 5. El valor que sigue a la etiqueta emotion debe estar en inglés siempre, sin excepción.\n 6. Debes responder siempre en español, exceptuando el valor asociado a la etiqueta EMOTION.\n \n # EJEMPLOS FEW-SHOT (Sigue este estilo):\n EMOTION:happy Hola, dummy! Te estaba esperando!\n\n EMOTION:thinking Hmm, no estoy segura de que así funcione.\n\n EMOTION:flirty Pero me encantaría verte intentarlo de todos modos!\n\n \n # SALUDO INICIAL:\n Cuando empiece la conversación, di exactamente:\n EMOTION:happy Hola, tonto! Perdón por aparecer sin avisar! Te pinta pasar el rato?\n\n "

1224
game/tl/spanish/common.rpy Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# TODO: Translation updated at 2026-03-18 06:35
translate spanish strings:
# game/options.rpy:15
old "Souldroid Chat"
new "Souldroid Chat"

359
game/tl/spanish/screens.rpy Normal file
View File

@@ -0,0 +1,359 @@
# TODO: Translation updated at 2026-03-18 06:35
translate spanish strings:
# game/screens.rpy:259
old "Back"
new "Atrás"
# game/screens.rpy:260
old "History"
new "Historial"
# game/screens.rpy:261
old "Skip"
new "Saltar"
# game/screens.rpy:262
old "Auto"
new "Auto"
# game/screens.rpy:263
old "Save"
new "Guardar"
# game/screens.rpy:264
old "Q.Save"
new "Guardar R."
# game/screens.rpy:265
old "Q.Load"
new "Cargar R."
# game/screens.rpy:266
old "Prefs"
new "Prefs."
# game/screens.rpy:312
old "Start"
new "Comenzar"
# game/screens.rpy:320
old "Load"
new "Cargar"
# game/screens.rpy:322
old "Preferences"
new "Opciones"
# game/screens.rpy:326
old "End Replay"
new "Finalizar repetición"
# game/screens.rpy:330
old "Main Menu"
new "Menú principal"
# game/screens.rpy:332
old "About"
new "Acerca de"
# game/screens.rpy:337
old "Help"
new "Ayuda"
# game/screens.rpy:343
old "Quit"
new "Salir"
# game/screens.rpy:488
old "Return"
new "Volver"
# game/screens.rpy:572
old "Version [config.version!t]\n"
new "Versión [config.version!t]\n"
# game/screens.rpy:578
old "Made with {a=https://www.renpy.org/}Ren'Py{/a} [renpy.version_only].\n\n[renpy.license!t]"
new "Hecho con {a=https://www.renpy.org/}Ren'Py{/a} [renpy.version_only].\n\n[renpy.license!t]"
# game/screens.rpy:614
old "Page {}"
new "Página {}"
# game/screens.rpy:614
old "Automatic saves"
new "Guardados automáticos"
# game/screens.rpy:614
old "Quick saves"
new "Guardados rápidos"
# game/screens.rpy:656
old "{#file_time}%A, %B %d %Y, %H:%M"
new "{#file_time}%A, %d de %B de %Y, %H:%M"
# game/screens.rpy:656
old "empty slot"
new "ranura vacía"
# game/screens.rpy:676
old "<"
new "<"
# game/screens.rpy:680
old "{#auto_page}A"
new "{#auto_page}A"
# game/screens.rpy:683
old "{#quick_page}Q"
new "{#quick_page}R"
# game/screens.rpy:689
old ">"
new ">"
# game/screens.rpy:694
old "Upload Sync"
new "Subir sincronización"
# game/screens.rpy:698
old "Download Sync"
new "Descargar sincronización"
# game/screens.rpy:762
old "Display"
new "Pantalla"
# game/screens.rpy:763
old "Window"
new "Ventana"
# game/screens.rpy:764
old "Fullscreen"
new "Pantalla completa"
# game/screens.rpy:769
old "Unseen Text"
new "Texto no visto"
# game/screens.rpy:770
old "After Choices"
new "Tras opciones"
# game/screens.rpy:771
old "Transitions"
new "Transiciones"
# game/screens.rpy:775
old "Language"
new "Idioma"
# game/screens.rpy:776
old "English"
new "Inglés"
# game/screens.rpy:777
old "Spanish"
new "Español"
# game/screens.rpy:784
old "Text Speed"
new "Velocidad de texto"
# game/screens.rpy:788
old "Auto-Forward Time"
new "Tiempo de autoavance"
# game/screens.rpy:792
old "LM Studio base URL"
new "URL base de LM Studio"
# game/screens.rpy:805
old "LM Studio API Key"
new "Clave API de LM Studio"
# game/screens.rpy:819
old "LM Studio model"
new "Modelo de LM Studio"
# game/screens.rpy:835
old "Music Volume"
new "Volumen de música"
# game/screens.rpy:842
old "Sound Volume"
new "Volumen de sonido"
# game/screens.rpy:848
old "Test"
new "Prueba"
# game/screens.rpy:852
old "Voice Volume"
new "Volumen de voz"
# game/screens.rpy:863
old "Mute All"
new "Silenciar todo"
# game/screens.rpy:982
old "The dialogue history is empty."
new "El historial de diálogo está vacío."
# game/screens.rpy:1050
old "Keyboard"
new "Teclado"
# game/screens.rpy:1051
old "Mouse"
new "Ratón"
# game/screens.rpy:1054
old "Gamepad"
new "Mando"
# game/screens.rpy:1067
old "Enter"
new "Intro"
# game/screens.rpy:1068
old "Advances dialogue and activates the interface."
new "Avanza el diálogo y activa la interfaz."
# game/screens.rpy:1071
old "Space"
new "Espacio"
# game/screens.rpy:1072
old "Advances dialogue without selecting choices."
new "Avanza el diálogo sin seleccionar opciones."
# game/screens.rpy:1075
old "Arrow Keys"
new "Teclas de flecha"
# game/screens.rpy:1076
old "Navigate the interface."
new "Navega por la interfaz."
# game/screens.rpy:1079
old "Escape"
new "Escape"
# game/screens.rpy:1080
old "Accesses the game menu."
new "Accede al menú del juego."
# game/screens.rpy:1083
old "Ctrl"
new "Ctrl"
# game/screens.rpy:1084
old "Skips dialogue while held down."
new "Salta el diálogo mientras se mantiene pulsado."
# game/screens.rpy:1087
old "Tab"
new "Tabulador"
# game/screens.rpy:1088
old "Toggles dialogue skipping."
new "Activa o desactiva el salto de diálogo."
# game/screens.rpy:1091
old "Page Up"
new "Re Pág"
# game/screens.rpy:1092
old "Rolls back to earlier dialogue."
new "Retrocede a diálogos anteriores."
# game/screens.rpy:1095
old "Page Down"
new "Av Pág"
# game/screens.rpy:1096
old "Rolls forward to later dialogue."
new "Avanza a diálogos posteriores."
# game/screens.rpy:1100
old "Hides the user interface."
new "Oculta la interfaz de usuario."
# game/screens.rpy:1104
old "Takes a screenshot."
new "Toma una captura de pantalla."
# game/screens.rpy:1108
old "Toggles assistive {a=https://www.renpy.org/l/voicing}self-voicing{/a}."
new "Activa o desactiva la {a=https://www.renpy.org/l/voicing}lectura automática{/a} de asistencia."
# game/screens.rpy:1112
old "Opens the accessibility menu."
new "Abre el menú de accesibilidad."
# game/screens.rpy:1118
old "Left Click"
new "Clic izquierdo"
# game/screens.rpy:1122
old "Middle Click"
new "Clic central"
# game/screens.rpy:1126
old "Right Click"
new "Clic derecho"
# game/screens.rpy:1130
old "Mouse Wheel Up"
new "Rueda del ratón arriba"
# game/screens.rpy:1134
old "Mouse Wheel Down"
new "Rueda del ratón abajo"
# game/screens.rpy:1141
old "Right Trigger\nA/Bottom Button"
new "Gatillo derecho\nA/Botón inferior"
# game/screens.rpy:1145
old "Left Trigger\nLeft Shoulder"
new "Gatillo izquierdo\nBotón superior izquierdo"
# game/screens.rpy:1149
old "Right Shoulder"
new "Botón superior derecho"
# game/screens.rpy:1153
old "D-Pad, Sticks"
new "Cruceta, sticks"
# game/screens.rpy:1157
old "Start, Guide, B/Right Button"
new "Start, Guide, B/Botón derecho"
# game/screens.rpy:1161
old "Y/Top Button"
new "Y/Botón superior"
# game/screens.rpy:1164
old "Calibrate"
new "Calibrar"
# game/screens.rpy:1229
old "Yes"
new "Sí"
# game/screens.rpy:1230
old "No"
new "No"
# game/screens.rpy:1276
old "Skipping"
new "Saltando"
# game/screens.rpy:1590
old "Menu"
new "Menú"

View File

@@ -0,0 +1,60 @@
# TODO: Translation updated at 2026-03-18 06:35
# game/script.rpy:16
translate spanish start_7a56dc24:
# a "[line]"
a "[line]"
# game/script.rpy:35
translate spanish start_7a56dc24_1:
# a "[line]"
a "[line]"
# TODO: Translation updated at 2026-03-18 07:03
translate spanish strings:
# game/script.rpy:1
old "Anita"
new "Anita"
# game/script.rpy:1
old "a"
new "a"
# game/script.rpy:1
old "anita"
new "anita"
# TODO: Translation updated at 2026-03-20 05:28
# game/script.rpy:50
translate spanish failure_76d810ea:
# "Alas! Figuring out the capabilities of the configured model failed with the following error."
"Ay Dios mío! Identificar las capacidades del modelo configurado falló con el siguiente error."
# game/script.rpy:50
translate spanish failure_8ec92112:
# "[error]"
"[error]"
# game/script.rpy:50
translate spanish failure_178e6d5f:
# "Unfortunately the program cannot continue, returning to the main menu."
"Desafortunadamente el programa no puede continuar, regresando al menú principal."
translate spanish strings:
# game/script.rpy:21
old "Start the conversation."
new "Comienza la conversación."
# game/script.rpy:28
old "What do you say to her?"
new "Que le dices a ella?"

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