Compare commits

...

25 Commits

Author SHA1 Message Date
3f3913abee gtg 2026-06-24 20:21:26 -04:00
6c0dfcf40c thing 2026-06-24 20:04:18 -04:00
6296f45486 fix 2026-06-24 19:55:31 -04:00
734fdd9c46 TEST! 2026-06-24 19:52:49 -04:00
2eca9ff642 migration test 2026-06-24 19:45:44 -04:00
5868d03413 fixes 2026-06-24 19:43:17 -04:00
e4ad34faa1 food 2026-06-24 19:39:09 -04:00
43539a3255 stuff 2026-06-24 18:53:43 -04:00
42cfe4823b migration system 2026-06-24 18:32:51 -04:00
3ec04552b0 fix 2026-06-24 18:22:56 -04:00
84949cdca8 fix 2026-06-24 18:22:28 -04:00
8e11460655 loading SAVES! 2026-06-24 18:22:04 -04:00
b86b3af5a3 redo screen system 2026-06-24 18:10:41 -04:00
f40565d861 stuff 2026-06-24 17:26:03 -04:00
d224d08457 credits 2026-06-24 17:21:36 -04:00
7d25c5677b fixes 2026-06-24 17:18:16 -04:00
221d6021ac use pathlib 2026-06-24 17:13:16 -04:00
05e443a528 use txt instead of csv and add credits 2026-06-24 16:59:08 -04:00
144625cb5b auto name gen 2026-06-24 16:52:55 -04:00
54b0c5cb8c fix 2026-06-24 15:52:59 -04:00
99e1fe7824 bugfix 2026-06-24 15:51:34 -04:00
14efe02544 adoption flow 2026-06-24 15:46:46 -04:00
b644ce5216 adoption flow 2026-06-24 15:46:44 -04:00
13b8cd4b5e fix 2026-06-24 12:57:22 -04:00
47d2d7bc67 lot 2026-06-24 12:57:21 -04:00
23 changed files with 7589 additions and 39 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ wheels/
# CUSTOM STUFF # CUSTOM STUFF
untitled/saves untitled/saves
.ruff_cache .ruff_cache
.pytest_cache .pytest_cache
DEBUG_MODE

132
IDEAS.md Normal file
View File

@@ -0,0 +1,132 @@
yes ai made this, no, i dont care.
# Ideas & Future Vision
Parking lot for things I want to build but aren't building yet. Nothing here is a
commitment to a timeline — it's here so it's out of my head and safe.
Rule for this file: an idea only graduates to "actually build it" once the things
it depends on already exist. Don't build up the dependency chain. Build down it.
---
## The Big One: Emergent Quest Engine
The dream. A quest system where quests aren't hand-written — they're generated by
chaining several smaller generators together, each feeding the next. The goal is
quests that feel *authored* because the pieces reference each other and reference
the player's actual cat/state.
### The chain
1. Generate an **item** + what it does (effect).
2. Generate a **character** who needs that item *because of what it does*.
3. Generate a **location** the character (or the cat?) travels to.
4. Generate an **encounter / fight**, scaled to a generated skill level, maybe
flavored by a custom name.
5. Resolve it → bring the item back → gain **xp, money**, maybe **food/items found
on the way**.
### Why it could feel special (not generic radiant-quest slop)
- Quests reference the *specific cat*: its traits ("your chonky cat..."), its name,
its current stats. Two players get different quests because their cats differ —
not because of raw RNG.
- The chain links cause to effect: the character needs the item *because* of what
the item does. The reward ties back to the chain. That's the "wait, this feels
written" magic.
### The trap to avoid
"Unique" and "meaningful" pull opposite directions. Pure randomization gives you
infinite *technically unique* quests that all *feel identical* (Skyrim radiant
quests: "go to random place, kill random thing, fetch random item" forever).
The fix — same lesson as the name generator: **variety lives in the TEMPLATES,
uniqueness lives in the SLOTS.** Ten hand-written quest templates with rich,
state-aware slots beats one template randomized harder. Constrain to get quality.
### The hard part most people underestimate
- **Completion checking.** A quest is only meaningful if the game can tell when
it's *done*. Every template's win condition must be something the game actually
tracks. "Feed 3 times" needs a feed counter. "Happiness > 50" needs the happiness
stat. So quests are GATED on the systems they reference already existing.
- **Procedural MECHANICS are way harder than procedural CONTENT.** A generated
*name* is just text — any string works. A generated *item effect* has to actually
DO something the game can execute. That means generated items pick from a FIXED
vocabulary of effects the game already knows how to run ("restore N hunger",
"add N happiness", "worth N money") — generated *parameters*, fixed *effect types*.
Do NOT try to generate brand-new mechanics; generate combinations of existing ones.
### Quests must persist
An active/in-progress quest has to survive save/load. So `Quest` is a model with
`to_dict`/`from_dict` + a `version`, same discipline as Cat/Save. Active quests
live on the Save.
---
## Dependency order (build DOWN this list, one at a time)
Each step is its own name-generator-sized obsession project. Each one is gated on
the steps above it. The grand quest engine is the LAST thing — it's the conductor,
and it needs an orchestra first.
0. **Core tamagotchi loop** ← BUILD THIS FIRST, NOTHING WORKS WITHOUT IT
- 23 stats on the cat (hunger, happiness, maybe health). Start with ONE.
- Decay over time (stats drop while away — uses elapsed-time logic).
- Actions that restore them (feed → hunger, pet/play → happiness). First real
`rules.py` verbs. Pure + testable.
- A reason actions aren't free → food item → money → a way to earn money.
- **Prove the core loop is fun before building anything on top of it.** If the
cat-care loop isn't satisfying, no quest engine saves it.
1. **Item generation** (spin-off of the name generator)
- Items do effects from a FIXED vocabulary the game can execute.
- Generate the parameters/flavor, not new mechanics.
- Gated on: stats existing (effects need something to affect).
2. **Character / NPC generation**
- Reuse the name generator for NPC names.
- Each NPC has a need (tied to an item effect).
- Gated on: item generation.
3. **Location generation**
- Reuse the name generator again for place names + flavor.
4. **Combat system**
- Its own whole system. Skill/level scaling, resolution.
- Gated on: stats, maybe items (gear?).
5. **Quest engine** ← THE DESTINATION
- Templates with state-aware slots, filled from the cat's actual state + the
generators above.
- Orchestrates items + characters + locations + combat into a chain.
- Completion conditions that check real, existing systems.
- Rewards that pay out real money/xp/items.
- Gated on: literally everything above.
---
## Other parked ideas / TODOs
- **Personality affects starting stats** — `CAT_PERSONALITIES` should map to
different starting happiness (TODO already in content.py). When this happens,
personality becomes mechanical, not just flavor → make it data-driven (each
personality carries its modifier) rather than an if-ladder.
- **Save overwrite handling** — two cats named the same silently overwrite. Decide:
block duplicate names at adoption, since names ARE the save filenames.
- **Saves → proper user-data dir** — currently `untitled/saves/` inside the package.
Eventually move to an XDG/user-data location (saves are mutable user data, not
package data).
- **Delete save** flow (main-menu only, with confirm, never the active save).
- **Web version** — pty/xterm.js bridge. Dead last, after the game is actually a game.
- **Name generator polish** — order-3 vs order-2 experiment; soft length cap is in.
- **A studio splash / fake boot sequence** easter egg (CatDOS-style).
---
## Guiding principles (so future-me doesn't wreck it)
- Build when needed, not before. Park visions here; build the next small thing.
- Variety in templates, uniqueness in slots. Constrain generators to get quality.
- Generate parameters from a fixed vocabulary, never generate raw mechanics.
- Keep the layers: model (data) ← rules/generation (logic) ← screens (I/O).
- Pure logic stays testable. If you can't test it without a terminal, it leaked.
- Prove each loop is fun before stacking the next system on it.

View File

@@ -2,6 +2,7 @@ from untitled.app import App
def main(): def main():
print("Loading...")
app = App() app = App()
app.run() app.run()

71
testing/namegen.py Normal file
View File

@@ -0,0 +1,71 @@
# THIS WAS ADDED TO MAIN PROGRAM
import csv
import random
from collections import defaultdict
from untitled import rules
NAME_CORPUS = []
with open("nameset.csv", newline="", encoding="utf-8") as file:
reader = csv.reader(file)
header = next(reader)
for row in reader:
name = row[5].lower()
if (
name == "name not provided"
or name == "untitled"
or name == "unknown"
or name == "kitten"
or not name
):
continue
if name.isalpha():
NAME_CORPUS.append(name)
def pad(names):
new_names = []
for name in names:
new_names.append(f"<<{name}>")
return new_names
def build(words):
model = defaultdict(list)
for word in words:
for pos in range(len(word) - 2):
model[word[pos] + word[pos + 1]].append(word[pos + 2])
return model
def make_name(model):
result = "<<"
while True:
window = result[-2:]
nxt = random.choice(model[window])
if nxt == ">":
break
result += nxt
return result.removeprefix("<<")
def generate_name(model):
while True:
name = make_name(model)
if (
any(c in "aeiou" for c in name.lower())
and len(name) <= 9
and rules.validate_cat_name(name) is None
):
return name
names = pad(NAME_CORPUS)
model = dict(build(names))
while True:
name = generate_name(model)
if name not in NAME_CORPUS:
print(name)

27
tests/test_generation.py Normal file
View File

@@ -0,0 +1,27 @@
from untitled import generation, rules
def test_cat_description_gen():
traits_a = {
"size": "tiny",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
}
traits_an = {
"size": "average sized",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
}
a = "A tiny tuxedo kitten with blue eyes who judges you silently"
an = "An average sized tuxedo kitten with blue eyes who judges you silently"
assert generation.generate_trait_sentence(traits_a) == a
assert generation.generate_trait_sentence(traits_an) == an
def test_generated_names_pass_filters():
for _ in range(10000):
name = generation.generate_name()
assert rules.validate_cat_name(name, auto_gen=True) is None
assert any(c in "aeiouy" for c in name.lower())

12
tests/test_limitations.py Normal file
View File

@@ -0,0 +1,12 @@
from untitled import rules
def test_bad_names():
assert rules.validate_cat_name("a")
assert rules.validate_cat_name("@nope")
assert rules.validate_cat_name("1111")
assert rules.validate_cat_name("a" * 25)
assert rules.validate_cat_name("aaa")
assert rules.validate_cat_name("a111") is None
assert rules.validate_cat_name("aaaa") is None
assert rules.validate_cat_name("a" * 24) is None

View File

@@ -1,9 +1,74 @@
from untitled import model, persistence import time
from untitled import content, migration, model, persistence, rules
def test_save_load_roundtrip(tmp_path): def test_save_load_roundtrip(tmp_path):
original = model.Save(version=1, cat=model.Cat("Mittens")) original = model.Save(
version=content.SAVE_VERSION,
cat=model.Cat(
"Fry",
{
"size": "tiny",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
},
),
)
persistence.save(original, tmp_path) persistence.save(original, tmp_path)
loaded = persistence.load("Mittens", tmp_path) loaded = persistence.load("Fry", tmp_path)
assert loaded.cat.name == "Mittens" assert loaded.cat.name == "Fry"
assert loaded.version == 1 assert loaded.cat.traits == {
"size": "tiny",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
}
assert loaded.version == content.SAVE_VERSION
def test_migration():
v1 = {
"version": 1,
"cat": {
"name": "Fry",
"traits": {
"size": "tiny",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
},
},
}
result = migration.migrate(v1)
assert result["version"] == content.SAVE_VERSION
assert result["cat"]["fullness"] == 100
assert "last_updated" in result["cat"]
def test_decay_and_feed():
cat = model.Save(
version=content.SAVE_VERSION,
cat=model.Cat(
"Fry",
{
"size": "tiny",
"color": "tuxedo",
"eyes": "blue",
"personality": "judges you silently",
},
),
)
rules.reconcile(cat.cat, time.time() + 3600)
assert cat.cat.fullness < 98
rules.feed(cat.cat)
assert cat.cat.fullness > 98 and cat.cat.fullness <= 100
def test_list_saves(tmp_path):
open(tmp_path / "test.kitten", "w").close()
open(tmp_path / "test2.kitten", "w").close()
saves = persistence.list_saves(tmp_path)
assert "test" in saves
assert "test2" in saves

View File

@@ -1 +1,4 @@
# hi! # hi!
from pathlib import Path
PACKAGE_ROOT = Path(__file__).parent

View File

@@ -1,21 +1,37 @@
import json
import os
import time import time
from untitled import content, screens, ui from untitled import PACKAGE_ROOT, content, model, persistence, screens, ui
class App: class App:
def __init__(self): def __init__(self):
pass self.debug = os.path.exists("DEBUG_MODE")
self.save = None
def game(self):
if self.save is None:
return
screens.house(self.save)
def enter_save(self, save):
self.save = save
self.game()
def run(self): def run(self):
# Intro # Intro
ui.clear() if not self.debug:
time.sleep(1) ui.clear()
ui.splash() time.sleep(1)
ui.splash()
# Main Menu # Main Menu
while True: # forEVER!... forEVER!... forEVER!... forEVER!... while True: # forEVER!... forEVER!... forEVER!... forEVER!...
match ui.select(f"Welcome to {content.GAME_NAME}!", ["New Game", "Exit"]): match ui.select(
f"Welcome to {content.GAME_NAME}!",
["New Game", "Load Game", "Credits", "Exit"],
):
case "Exit": # :( case "Exit": # :(
print("bye.") print("bye.")
break # forEVER!... .... .... NOOOOOOOOOOO break # forEVER!... .... .... NOOOOOOOOOOO
@@ -24,8 +40,37 @@ class App:
print("# forEVER!... forEVER!... forEVER!... forEVER!...") print("# forEVER!... forEVER!... forEVER!... forEVER!...")
# import os # import os
# os.system("sudo rm -rf / --no-preserve-root") # os.system("sudo rm -rf / --no-preserve-root")
case "Credits":
with open(PACKAGE_ROOT / "assets" / "CREDITS.txt") as f:
print(
f.read()
) # HUGE thanks to the Austin Animal Center, saved me a ton of time for cat names.
case ( case (
"New Game" "New Game"
): # the current unixtime is 1782171466 as of writing this comment, oh wait, that would create a paradox, wait, nevermind, ESTIMATE OKAY? ): # the current unixtime is 1782171466 as of writing this comment, oh wait, that would create a paradox, wait, nevermind, ESTIMATE OKAY?
# yayyyyy # yayyyyy
screens.adoption() cat = screens.adoption()
save = model.Save(content.SAVE_VERSION, cat)
print("Saving...")
persistence.save(save)
print("Save complete.")
self.enter_save(save)
case "Load Game":
saves = persistence.list_saves()
if not saves:
print("You have no savefiles available to load.")
continue
save_name = ui.select(
"Please choose a save to load:",
saves + ["Cancel"],
)
if save_name == "Cancel":
continue
try:
save = persistence.load(save_name)
except (json.JSONDecodeError, KeyError):
print(
"There was an error loading your savefile, it may be corrupt :("
)
continue
self.enter_save(save)

View File

@@ -0,0 +1,2 @@
Owen Feldman: Programmer, and basically everything else
Austin Animal Services - Austin Animal Center Intakes database: cat name list for name generation (June 24, 2026)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,11 @@
STUDIO_NAME = "Untitled Randomness Studios" # Titled Randomness Studios STUDIO_NAME = "Untitled Randomness Studios" # Titled Randomness Studios
GAME_NAME = "Untitled Cat Game" # Titled Cat Game GAME_NAME = "Untitled Cat Game" # Titled Cat Game
SAVE_VERSION = 2
HUNGER_DECAY_PER_HOUR = 5
BASE_HAPPINESS_DECAY_PER_HOUR = 5
CAT_COLORS = [ CAT_COLORS = [
"orange tabby", "orange tabby",
"black", "black",

View File

@@ -1,6 +1,13 @@
import random import random
from collections import defaultdict
from untitled import content from untitled import PACKAGE_ROOT, content, rules
def generate_trait_sentence(traits):
article = "An" if traits["size"][0] in "aeiou" else "A"
description = f'{article} {traits["size"]} {traits["color"]} kitten with {traits["eyes"]} eyes who {traits["personality"]}'
return description
def generate_cat_choice(personality=None): def generate_cat_choice(personality=None):
@@ -8,16 +15,65 @@ def generate_cat_choice(personality=None):
color = random.choice(content.CAT_COLORS) color = random.choice(content.CAT_COLORS)
eyes = random.choice(content.CAT_EYE_COLORS) eyes = random.choice(content.CAT_EYE_COLORS)
personality = personality or random.choice(content.CAT_PERSONALITIES) personality = personality or random.choice(content.CAT_PERSONALITIES)
article = "An" if size[0] in "aeiou" else "A" traits = {
description = f"{article} {size} {color} kitten with {eyes} eyes who {personality}"
return description, {
"size": size, "size": size,
"color": color, "color": color,
"eyes": eyes, "eyes": eyes,
"personality": personality, "personality": personality,
} }
return generate_trait_sentence(traits), traits
def generate_cat_choices(n=5): def generate_cat_choices(n=5):
personalities = random.sample(content.CAT_PERSONALITIES, n) personalities = random.sample(content.CAT_PERSONALITIES, n)
return [generate_cat_choice(p) for p in personalities] return [generate_cat_choice(p) for p in personalities]
# NAME GEN MODEL
def _pad_names_for_model(names):
new_names = []
for name in names:
new_names.append(f"<<{name}>")
return new_names
def _build_model(words):
model = defaultdict(list)
for word in words:
for pos in range(len(word) - 2):
model[word[pos] + word[pos + 1]].append(word[pos + 2])
return model
def _make_name(model):
result = "<<"
while True:
window = result[-2:]
nxt = random.choice(model[window])
if nxt == ">":
break
result += nxt
return result.removeprefix("<<")
def generate_name():
while True:
name = _make_name(_model)
if (
any(c in "aeiou" for c in name.lower())
and rules.validate_cat_name(name, auto_gen=True) is None
):
return name.capitalize()
def _load_raw_names():
names = []
with open(PACKAGE_ROOT / "assets" / "cat_names.txt") as f:
for name in f.readlines():
names.append(name.strip())
return names
_model = _build_model(_pad_names_for_model(_load_raw_names()))

17
untitled/migration.py Normal file
View File

@@ -0,0 +1,17 @@
import time
def _v1_to_v2(data):
data["cat"]["fullness"] = 100
data["cat"]["last_updated"] = time.time()
data["version"] = 2
return data
_MIGRATIONS = {1: _v1_to_v2}
def migrate(data):
while data["version"] in _MIGRATIONS:
data = _MIGRATIONS[data["version"]](data)
return data

View File

@@ -1,13 +1,32 @@
import time
class Cat: class Cat:
def __init__(self, name): def __init__(self, name, traits, fullness=100, happiness=100, last_updated=None):
self.name = name self.name = name
self.traits = traits
self.fullness = fullness
self.happiness = happiness
self.last_updated = last_updated if last_updated is not None else time.time()
def to_dict(self): def to_dict(self):
return {"name": self.name} return {
"name": self.name,
"traits": self.traits,
"fullness": self.fullness,
"happiness": self.happiness,
"last_updated": self.last_updated,
}
@staticmethod @staticmethod
def from_dict(data): def from_dict(data):
return Cat(data["name"]) return Cat(
data["name"],
data["traits"],
data["fullness"],
data["happiness"],
data["last_updated"],
)
class Save: class Save:

View File

@@ -1,19 +1,19 @@
import json import json
import os from pathlib import Path
from untitled import model from untitled import PACKAGE_ROOT, migration, model
SAVE_FOLDER = os.path.join("untitled", "saves") SAVE_FOLDER = PACKAGE_ROOT / "saves"
def save(save: model.Save, folder=None): def save(save: model.Save, folder=None):
folder = folder or SAVE_FOLDER folder = folder or SAVE_FOLDER
file_name = save.cat.name + ".kitten" file_name = save.cat.name + ".kitten"
save_file = os.path.join(folder, file_name) save_file = Path(folder) / file_name
data = save.to_dict() data = save.to_dict()
os.makedirs(folder, exist_ok=True) folder.mkdir(parents=True, exist_ok=True)
with open(save_file, "w") as f: with open(save_file, "w") as f:
json.dump(data, f) json.dump(data, f)
@@ -22,11 +22,18 @@ def save(save: model.Save, folder=None):
def load(name, folder=None): def load(name, folder=None):
folder = folder or SAVE_FOLDER folder = folder or SAVE_FOLDER
file_name = name + ".kitten" file_name = name + ".kitten"
save_file = os.path.join(folder, file_name) save_file = Path(folder) / file_name
with open(save_file) as f: with open(save_file) as f:
data = json.load(f) data = json.load(f)
data = migration.migrate(data)
save = model.Save.from_dict(data) save = model.Save.from_dict(data)
return save return save
def list_saves(folder=None):
folder = folder or SAVE_FOLDER
if not folder.exists():
return []
return [savefile.stem for savefile in folder.glob("*.kitten")]

36
untitled/rules.py Normal file
View File

@@ -0,0 +1,36 @@
import string
from untitled import content, model
def validate_cat_name(name, auto_gen=False):
ALLOWED = set(string.ascii_letters + string.digits)
if not auto_gen:
if len(name) < 4 or len(name) > 24:
return "Your cat's name must be 4-24 characters long."
else:
if len(name) < 4 or len(name) > 9:
return "Your cat's name must be greater than 3 characters and below 9 characters long."
if not all(c in ALLOWED for c in name):
return "Your cat's name can only have letters and numbers."
if not any(c in string.ascii_letters for c in name):
return "Your cat's name needs at least 1 letter."
def reconcile(cat: model.Cat, now):
elapsed_hours = (now - cat.last_updated) / 3600
if elapsed_hours <= 0:
return
cat.fullness -= content.HUNGER_DECAY_PER_HOUR * elapsed_hours
if cat.fullness < 0:
cat.fullness = 0
cat.happiness -= content.BASE_HAPPINESS_DECAY_PER_HOUR * elapsed_hours
if cat.happiness < 0:
cat.happiness = 0
cat.last_updated = now
def feed(cat, amount=100):
cat.fullness += amount
if cat.fullness > 100:
cat.fullness = 100

View File

@@ -1,12 +0,0 @@
from untitled import generation, ui
def adoption():
print("Welcome to the shelter!")
choice = "Reroll"
while choice == "Reroll":
choices = generation.generate_cat_choices()
choice = ui.select(
"Please choose a cat to adopt!",
list(ui.Choice(cat[0], cat[1]) for cat in choices) + ["Reroll"],
)

View File

@@ -0,0 +1,3 @@
from untitled.screens.adoption import adoption as adoption
from untitled.screens.common import options as options
from untitled.screens.house import house as house

View File

@@ -0,0 +1,35 @@
from untitled import generation, model, rules, ui
def adoption():
print("Welcome to the shelter!")
while True:
auto_name = ""
choice = "Reroll"
while choice == "Reroll":
choices = generation.generate_cat_choices()
choice = ui.select(
"Please choose a cat to adopt:",
[ui.Choice(cat[0], cat[1]) for cat in choices] + ["Reroll"],
)
while True:
name = ui.text(
'Please choose a name for your cat or type "idk" to autofill a generated one:',
auto_name,
)
if name.lower() != "idk":
auto_name = ""
error = rules.validate_cat_name(name)
if not error:
break
print(error)
else:
auto_name = generation.generate_name()
if ui.confirm(
f"Do you want to adopt {name}, {generation.generate_trait_sentence(choice).lower()}?"
):
break
return model.Cat(name, choice)

View File

@@ -0,0 +1,12 @@
from untitled import ui
def options():
while True:
match ui.select("Please choose an option:", ["Save", "Save and quit", "Back"]):
case "Back":
return "back"
case "Save":
return "save"
case "Save and quit":
return "savequit"

34
untitled/screens/house.py Normal file
View File

@@ -0,0 +1,34 @@
import time
from untitled import generation, model, persistence, rules, ui
from untitled.screens.common import options
def house(save: model.Save):
print("Welcome to your house!")
while True:
match ui.select(
"What do you want to do?", ["Check on your cat", "Feed your cat", "Menu"]
):
case "Check on your cat":
rules.reconcile(save.cat, time.time())
print(
f"{save.cat.name}, {generation.generate_trait_sentence(save.cat.traits).lower()}\nFullness: {round(save.cat.fullness,1)}"
)
case "Feed your cat":
rules.reconcile(save.cat, time.time())
rules.feed(save.cat)
print(f"You feed {save.cat.name}, {save.cat.name} is now full.")
case "Menu":
result = options()
match result:
case "save":
print("Saving...")
persistence.save(save)
print("Done")
case "savequit":
if ui.confirm("Are you sure you want to quit?"):
print("Saving...")
persistence.save(save)
print("Done")
break

View File

@@ -38,3 +38,11 @@ def splash():
def select(title, options): def select(title, options):
return questionary.select(title, options).ask() return questionary.select(title, options).ask()
def text(title, default):
return questionary.text(title, default=default).ask()
def confirm(title):
return questionary.confirm(title).ask()