Compare commits

...

14 Commits

Author SHA1 Message Date
6b44022022 depression+sick+inventory+pain 2026-06-26 19:30:30 -04:00
7e85543c21 oops 2026-06-25 17:05:35 -04:00
f1ea9a1de5 stuff 2026-06-25 16:56:33 -04:00
0b0ae5db73 fix 2026-06-25 16:37:40 -04:00
23b1b83bdd oops 2026-06-25 16:37:02 -04:00
19d7c76374 stuff 2026-06-25 16:36:44 -04:00
96b5ab2069 fix 2026-06-25 16:34:26 -04:00
188c1be7bf inventory and feed 2026-06-25 16:32:40 -04:00
f61889195c work! 2026-06-25 16:02:01 -04:00
c1a5361255 shop2 2026-06-25 15:40:45 -04:00
988ddf1ebf shop 2026-06-25 15:40:40 -04:00
fc81a46dd9 test 2026-06-25 14:49:38 -04:00
a2bd0ded1b oop 2026-06-25 14:46:31 -04:00
55d1a66cfb player object 2026-06-25 14:46:30 -04:00
12 changed files with 401 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from untitled import content, migration, model, persistence, rules from untitled import content, model, persistence, rules
def test_save_load_roundtrip(tmp_path): def test_save_load_roundtrip(tmp_path):
@@ -25,26 +25,27 @@ def test_save_load_roundtrip(tmp_path):
"eyes": "blue", "eyes": "blue",
"personality": "judges you silently", "personality": "judges you silently",
} }
assert loaded.player.inventory == content.BASE_INVENTORY
assert loaded.version == content.SAVE_VERSION assert loaded.version == content.SAVE_VERSION
def test_migration(): # def test_migration():
v1 = { # v1 = {
"version": 1, # "version": 1,
"cat": { # "cat": {
"name": "Fry", # "name": "Fry",
"traits": { # "traits": {
"size": "tiny", # "size": "tiny",
"color": "tuxedo", # "color": "tuxedo",
"eyes": "blue", # "eyes": "blue",
"personality": "judges you silently", # "personality": "judges you silently",
}, # },
}, # },
} # }
result = migration.migrate(v1) # result = migration.migrate(v1)
assert result["version"] == content.SAVE_VERSION # assert result["version"] == content.SAVE_VERSION
assert result["cat"]["fullness"] == 100 # assert result["cat"]["fullness"] == 100
assert "last_updated" in result["cat"] # assert "last_updated" in result["cat"]
def test_decay_and_replenish(): def test_decay_and_replenish():
@@ -61,10 +62,11 @@ def test_decay_and_replenish():
last_updated=0, last_updated=0,
), ),
) )
cat.player.inventory["food"] = 1
rules.reconcile(cat.cat, 3600 * 2) rules.reconcile(cat.cat, 3600 * 2)
assert cat.cat.happiness < 98 assert cat.cat.happiness < 98
assert cat.cat.fullness < 98 assert cat.cat.fullness < 98
rules.feed(cat.cat) rules.feed(cat.player, cat.cat)
assert cat.cat.fullness == pytest.approx(100) assert cat.cat.fullness == pytest.approx(100)
rules.reconcile(cat.cat, 7200 + 20 * 3600) rules.reconcile(cat.cat, 7200 + 20 * 3600)
assert cat.cat.happiness < 96 - (content.BASE_HAPPINESS_DECAY_PER_HOUR * 20) assert cat.cat.happiness < 96 - (content.BASE_HAPPINESS_DECAY_PER_HOUR * 20)

View File

@@ -1,12 +1,24 @@
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 SAVE_VERSION = 1
HUNGER_DECAY_PER_HOUR = 5 HUNGER_DECAY_PER_HOUR = 5
BASE_HAPPINESS_DECAY_PER_HOUR = 2 BASE_HAPPINESS_DECAY_PER_HOUR = 2
HUNGER_SADNESS_THRESHOLD = 30 HUNGER_SADNESS_THRESHOLD = 30
HUNGER_SADNESS_PENALTY_PER_HOUR = 5 HUNGER_SADNESS_PENALTY_PER_HOUR = 5
ITEMS = {"food": 3, "medicine": 5, "catnip": 5}
BASE_INVENTORY = {item: 0 for item in ITEMS}
WORK_START_LETTERS = 2
WORK_EARN_PER_ROUND = 3
FOOD_RESTORE = 30
CATNIP_RESTORE = 20
MEDICINE_RESTORE = 20
CAT_COLORS = [ CAT_COLORS = [
"orange tabby", "orange tabby",
"black", "black",

View File

@@ -1,15 +1,13 @@
import time
# def _v1_to_v2(data):
# data["cat"]["fullness"] = 100
# data["cat"]["happiness"] = 100
# data["cat"]["last_updated"] = time.time()
# data["version"] = 2
# return data
def _v1_to_v2(data): _MIGRATIONS = {}
data["cat"]["fullness"] = 100
data["cat"]["happiness"] = 100
data["cat"]["last_updated"] = time.time()
data["version"] = 2
return data
_MIGRATIONS = {1: _v1_to_v2}
def migrate(data): def migrate(data):

View File

@@ -1,6 +1,8 @@
import time import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from untitled import content
@dataclass @dataclass
class Cat: class Cat:
@@ -21,13 +23,31 @@ class Cat:
@dataclass @dataclass
class Save: class Player:
version: int money: int = 0
cat: Cat inventory: dict = field(default_factory=lambda: dict(content.BASE_INVENTORY))
def to_dict(self): def to_dict(self):
return asdict(self) return asdict(self)
@staticmethod @staticmethod
def from_dict(data): def from_dict(data):
return Save(data["version"], Cat.from_dict(data["cat"])) return Player(**data)
@dataclass
class Save:
version: int
cat: Cat
player: Player = field(default_factory=Player) # lazy :)
def to_dict(self):
return asdict(self)
@staticmethod
def from_dict(data):
return Save(
data["version"],
Cat.from_dict(data["cat"]),
Player.from_dict(data["player"]),
)

View File

@@ -41,14 +41,52 @@ def reconcile(cat: model.Cat, now):
cat.happiness -= content.HUNGER_SADNESS_PENALTY_PER_HOUR * elapsed_hours cat.happiness -= content.HUNGER_SADNESS_PENALTY_PER_HOUR * elapsed_hours
cat.happiness = _clamp(cat.happiness) cat.happiness = _clamp(cat.happiness)
if cat.happiness == 0:
cat.depressed = True
if cat.fullness == 0:
cat.sick = True
cat.last_updated = now cat.last_updated = now
def feed(cat, amount=100): def feed(player, cat, amount=content.FOOD_RESTORE):
cat.fullness += amount if player.inventory["food"] <= 0:
cat.fullness = _clamp(cat.fullness) return False
player.inventory["food"] -= 1
cat.fullness = _clamp(cat.fullness + amount)
return True
def cure_sick(save, restore=content.MEDICINE_RESTORE):
if save.player.inventory["medicine"] <= 0:
return False
save.cat.fullness = restore
save.cat.sick = False
save.player.inventory["medicine"] -= 1
return True
def cure_depressed(save, restore=content.CATNIP_RESTORE):
if save.player.inventory["catnip"] <= 0:
return False
save.cat.happiness = restore
save.cat.depressed = False
save.player.inventory["catnip"] -= 1
return True
def excite(cat, amount=100): def excite(cat, amount=100):
cat.happiness += amount cat.happiness += amount
cat.happiness = _clamp(cat.happiness) cat.happiness = _clamp(cat.happiness)
def buy(player, item, price):
if price > player.money:
return False
player.money -= price
player.inventory[item] += 1
return True
def earn(player, amount):
player.money += amount

View File

@@ -1,4 +1,5 @@
from untitled import ui from untitled import ui
from untitled.screens import shop, work
def options(): def options():
@@ -10,3 +11,11 @@ def options():
return "save" return "save"
case "Save and quit": case "Save and quit":
return "savequit" return "savequit"
def go_to(save):
match ui.select("Where do you want to go?", ["The shop", "Work", "Back"]):
case "The shop":
shop.shop(save)
case "Work":
work.work(save)

View File

@@ -1,29 +1,63 @@
import time import time
from untitled import generation, model, persistence, rules, ui from untitled import generation, model, persistence, rules, ui
from untitled.screens.common import options from untitled.screens import inventory, pet
from untitled.screens.common import go_to, options
def house(save: model.Save): def house(save: model.Save):
print("Welcome to your house!") print("Welcome to your house!")
if save.cat.sick or save.cat.depressed:
print(
f"Your cat is {"sick" if save.cat.sick else ""}{" and " if save.cat.sick and save.cat.depressed else ""}{"depressed" if save.cat.depressed else ""}. "
)
while True: while True:
match ui.select( match ui.select(
"What do you want to do?", "What do you want to do?",
["Check on your cat", "Feed your cat", "Pet your cat", "Menu"], [
"Check on your cat",
"View your inventory",
"Feed your cat",
"Pet your cat",
"Go to...",
"Menu",
],
): ):
case "Check on your cat": case "Check on your cat":
rules.reconcile(save.cat, time.time()) rules.reconcile(save.cat, time.time())
print( if save.cat.sick or save.cat.depressed:
f"{save.cat.name}, {generation.generate_trait_sentence(save.cat.traits).lower()}\nFullness: {round(save.cat.fullness,1)}\nHappiness: {round(save.cat.happiness,1)}" print(
) f"{save.cat.name}: a {"sick" if save.cat.sick else ""}{" and " if save.cat.sick and save.cat.depressed else ""}{"depressed" if save.cat.depressed else ""} cat."
)
else:
print(
f"{save.cat.name}, {generation.generate_trait_sentence(save.cat.traits).lower()}.\nFullness: {round(save.cat.fullness,1)}\nHappiness: {round(save.cat.happiness,1)}"
)
case "View your inventory":
print(f"Money: {save.player.money}")
if any(amount > 0 for amount in save.player.inventory.values()):
inventory.inventory(save)
else:
print("You have no items!")
case "Feed your cat": case "Feed your cat":
rules.reconcile(save.cat, time.time()) if not save.cat.sick:
rules.feed(save.cat) rules.reconcile(save.cat, time.time())
print(f"You feed {save.cat.name}, {save.cat.name} is now full.") if rules.feed(save.player, save.cat):
print(
f"You feed {save.cat.name}, {save.cat.name} is now {round(save.cat.fullness,1)}% full."
)
else:
print("You don't have any food!")
else:
print(f"{save.cat.name} is sick!")
case "Pet your cat": case "Pet your cat":
rules.reconcile(save.cat, time.time()) if not save.cat.depressed:
rules.excite(save.cat) rules.reconcile(save.cat, time.time())
print(f"You pet {save.cat.name}, {save.cat.name} is now happy.") pet.pet(save.cat)
else:
print(f"{save.cat.name} is depressed!")
case "Go to...":
go_to(save)
case "Menu": case "Menu":
result = options() result = options()
match result: match result:

View File

@@ -0,0 +1,77 @@
import time
from untitled import rules, ui
def use_medicine(save):
match ui.select(
"What do you want to do with the medicine?",
[f"Give to {save.cat.name}", "Back"],
):
case "Back":
return True # keep in inventory menu
case give if give == f"Give to {save.cat.name}":
if save.cat.sick:
rules.reconcile(save.cat, time.time())
if save.player.inventory["medicine"] > 0:
rules.cure_sick(save)
print(
f"You give {save.cat.name} the medicine. They start to feel better. You should probably still feed them."
)
else:
print(
"You don't have any medicine! (what?) (you shouldn't see this)"
)
return False
else:
print(f"{save.cat.name} isn't sick!")
return False
return False
def use_catnip(save):
match ui.select(
"What do you want to do with the catnip?",
[f"Give to {save.cat.name}", "Back"],
):
case "Back":
return True # keep in inventory menu
case give if give == f"Give to {save.cat.name}":
if save.cat.depressed:
rules.reconcile(save.cat, time.time())
if save.player.inventory["catnip"] > 0:
rules.cure_depressed(save)
print(
f"You give {save.cat.name} the catnip. They begin to feel happier. You should probably still pet them."
)
else:
print("You don't have any catnip! (what?) (you shouldn't see this)")
return False
else:
print(f"{save.cat.name} isn't depressed!")
return False
return False
def inventory(save):
while True:
item = ui.select(
"Please choose an item:",
[
item.capitalize()
for item in save.player.inventory
if save.player.inventory[item] > 0 and item != "food" # in main program
]
+ ["Cancel"],
)
if item == "Cancel":
return
match item:
case "Medicine":
stay = use_medicine(save)
case "Catnip":
stay = use_catnip(save)
case _:
stay = False
if not stay:
break

64
untitled/screens/pet.py Normal file
View File

@@ -0,0 +1,64 @@
from untitled import rules, ui
def pet(cat):
if cat.happiness == 100:
print("Your cat is already fully happy! They don't want any more pets.")
return
original_happiness = cat.happiness
print(f"Mash keys to pet {cat.name}, press enter when you're done.")
count = 0
last = None
print("\rPets: 0", end="", flush=True)
while True:
if count < 100000:
key = ui.getch()
if key in ("\r", "\n"):
print()
break
if key != last:
count += 1
last = key
print(f"\rPets: {count}", end="", flush=True)
else:
rules.excite(cat, -5)
print(f"\n{cat.name} ran away to protect your hands")
print(
f"You lost 5% happiness due to stressing your cat. Before petting, your cat was {round(original_happiness,1)}% happy. You lost {round(original_happiness-cat.happiness,1)}% happiness. Your cat is now {round(cat.happiness,1)}% happy."
)
return
add_happiness = 0
if count == 0:
print("You didn't pet your cat at all.")
return
elif count < 50:
add_happiness = 5
print(f"You didn't pet your cat enough, {cat.name} wants more pets.")
elif count <= 200:
add_happiness = 15
print(f"You pet {cat.name} a lot, {cat.name} is happy.")
elif count <= 500:
add_happiness = 20
print(f"You pet {cat.name} a lot. {cat.name} is very happy.")
elif count <= 1000:
add_happiness = 20
print(f"You pet {cat.name} an absurd amount of times.")
elif count <= 10000:
add_happiness = 15
print(f"{cat.name} has had enough pets.")
elif count <= 20000:
add_happiness = 10
print("What are you even doing at this point?")
elif count <= 50000:
add_happiness = 5
print("You should probably stop now.")
elif count <= 75000:
add_happiness = 2
print("Seriously. Stop.")
else:
add_happiness = 1
print(f"{cat.name} is getting extremely worried about your hands.")
rules.excite(cat, add_happiness)
print(
f"Your cat is now {round(cat.happiness,1)}% happy. Before petting, your cat was {round(original_happiness,1)}% happy. You gained {round(cat.happiness-original_happiness,1)}% happiness."
)

29
untitled/screens/shop.py Normal file
View File

@@ -0,0 +1,29 @@
from untitled import content, model, rules, ui
def shop(save: model.Save):
print("Welcome to the shop!")
while True:
match ui.select("What do you want to do?", ["Buy items", "Leave"]):
case "Buy items":
while True:
print(f"You have ${save.player.money}.")
item = ui.select(
"Please choose an item to buy:",
[
ui.Choice(f"{item.capitalize()}: ${price}", (item, price))
for item, price in content.ITEMS.items()
]
+ ["Back"],
)
if item == "Back":
break
if ui.confirm(
f"Are you sure you want to buy {item[0]} for ${item[1]}?"
):
if rules.buy(save.player, item[0], item[1]):
print("Done!")
else:
print("You don't have enough money!")
case "Leave":
break

51
untitled/screens/work.py Normal file
View File

@@ -0,0 +1,51 @@
import random
import string
import time
from untitled import content, rules, ui
def work(save):
length = content.WORK_START_LETTERS
total_earned = 0
lost = False
print("Welcome to work!")
print("The rules:")
print(
"Each round, a string of letters will appear, when it says go, type them from memory. If you miss one, you lose, each round gets more money."
)
if not ui.confirm("Would you like to start?"):
return
for i in range(3, 0, -1):
print(i)
time.sleep(1)
ui.clear()
print("Start!")
while not lost:
seconds = 1 + (length * 0.5)
letters = random.choices(string.ascii_lowercase, k=length)
print(f"Round {len(letters)-1}")
print("Memorize:", " ".join(letters))
for i in range(round(seconds), 0, -1):
print(i)
time.sleep(1)
ui.clear()
print(f"Round {len(letters)-1}")
print("Type!")
for letter in letters:
key = ui.getch().lower()
print(key, end=" ", flush=True)
if key != letter:
print(f"\nThe correct key was: {letter}")
lost = True
break
if not lost:
total_earned += content.WORK_EARN_PER_ROUND
print("\nCorrect!")
time.sleep(1)
ui.clear()
length += 1
rules.earn(save.player, total_earned)
print(
f"Game finished! You earned ${total_earned}, you now have ${save.player.money}!"
)

View File

@@ -1,3 +1,4 @@
import sys
import time import time
import questionary import questionary
@@ -46,3 +47,21 @@ def text(title, default):
def confirm(title): def confirm(title):
return questionary.confirm(title).ask() return questionary.confirm(title).ask()
def getch():
if sys.platform == "win32":
import msvcrt
return msvcrt.getwch()
else:
import termios
import tty
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)